nexus-prime 7.9.9 → 7.9.10

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.
@@ -7,17 +7,11 @@
7
7
  import { existsSync } from 'fs';
8
8
  import { homedir } from 'os';
9
9
  import { dirname, join, resolve } from 'path';
10
+ import { buildNexusMcpServerConfig } from '../../engines/mcp-entrypoint.js';
10
11
  let callerIDECache;
11
12
  /** Nexus Prime MCP server entry (stdio transport). */
12
13
  function nexusMcpServerEntry(workspaceRoot) {
13
- return {
14
- command: 'node',
15
- args: [join(workspaceRoot, 'node_modules', 'nexus-prime', 'dist', 'cli.js'), 'mcp'],
16
- env: {
17
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
18
- NEXUS_WORKSPACE_ROOT: workspaceRoot,
19
- },
20
- };
14
+ return buildNexusMcpServerConfig(workspaceRoot);
21
15
  }
22
16
  function hasAnyEnvPrefix(prefix) {
23
17
  return Object.keys(process.env).some((key) => key.startsWith(prefix));
@@ -11,6 +11,7 @@ import { dirname, join, resolve } from 'path';
11
11
  import { detectInstalledIDEs, getMcpConfigForIDE } from '../agents/adapters/ide-compat.js';
12
12
  import { DEFAULT_DAEMON_READY_TIMEOUT_MS, ensureDaemonReady } from '../daemon/client.js';
13
13
  import { getSetupDefinition, installSetup } from '../engines/client-bootstrap.js';
14
+ import { buildNexusMcpServerConfig } from '../engines/mcp-entrypoint.js';
14
15
  import { resolveNexusStateDir } from '../engines/runtime-registry.js';
15
16
  import { resolveWorkspaceContext } from '../engines/workspace-resolver.js';
16
17
  import { runPostinstallCleanup } from '../postinstall/cleanup.js';
@@ -136,14 +137,7 @@ export async function configureIDE(ide, opts = {}) {
136
137
  }
137
138
  /** Nexus Prime MCP server entry used in workspace-local config writes. */
138
139
  function _nexusMcpEntry(workspaceRoot) {
139
- return {
140
- command: 'node',
141
- args: [join(workspaceRoot, 'node_modules', 'nexus-prime', 'dist', 'cli.js'), 'mcp'],
142
- env: {
143
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
144
- NEXUS_WORKSPACE_ROOT: workspaceRoot,
145
- },
146
- };
140
+ return buildNexusMcpServerConfig(workspaceRoot);
147
141
  }
148
142
  /**
149
143
  * Write Claude Code PreToolUse/PostToolUse/UserPromptSubmit/Stop hooks to
@@ -379,18 +373,15 @@ function mergeIntoExistingConfig(configPath, newContent, ide) {
379
373
  }
380
374
  /** Print how to add nexus-prime manually to an MCP client. */
381
375
  export function printManualMcpInstructions() {
376
+ const server = buildNexusMcpServerConfig(process.cwd());
382
377
  const snippet = JSON.stringify({
383
378
  mcpServers: {
384
- 'nexus-prime': {
385
- command: 'node',
386
- args: ['node_modules/nexus-prime/dist/cli.js', 'mcp'],
387
- env: {},
388
- },
379
+ 'nexus-prime': server,
389
380
  },
390
381
  }, null, 2);
391
382
  process.stdout.write('\n Add this to your IDE\'s MCP configuration:\n\n');
392
383
  process.stdout.write(snippet.split('\n').map(l => ` ${l}`).join('\n') + '\n\n');
393
- process.stdout.write(' Or run: npx nexus-prime@latest setup\n\n');
384
+ process.stdout.write(' Or run: nexus-prime setup\n\n');
394
385
  }
395
386
  /** Entry point for CLI: nexus-prime install / setup auto */
396
387
  export async function cliSetup(opts = []) {
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { InstructionGateway } from './engines/instruction-gateway.js';
18
18
  import { ensureBootstrap, collectBootstrapManifest, validateTargetPath, resolveCodexConfigPath, renderCodexMcpTomlConfig, writeCodexMcpConfig, hasExpectedCodexConfig, } from './engines/client-bootstrap.js';
19
19
  import { resolveNexusStateDir } from './engines/runtime-registry.js';
20
20
  import { runStartupHygiene } from './engines/runtime-hygiene.js';
21
+ import { buildNexusMcpServerConfig, isStableNexusMcpServerConfig } from './engines/mcp-entrypoint.js';
21
22
  import { SessionDNAManager } from './engines/session-dna.js';
22
23
  import { nexusEventBus } from './engines/event-bus.js';
23
24
  import { buildRuntimeSetupCommand } from './cli-setup.js';
@@ -189,14 +190,7 @@ function readJson(targetPath) {
189
190
  }
190
191
  }
191
192
  function buildStandardMcpServerConfig(workspaceRoot) {
192
- return {
193
- command: 'npx',
194
- args: ['-y', 'nexus-prime', 'mcp'],
195
- env: {
196
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
197
- ...(workspaceRoot ? { NEXUS_WORKSPACE_ROOT: workspaceRoot } : {}),
198
- }
199
- };
193
+ return buildNexusMcpServerConfig(workspaceRoot);
200
194
  }
201
195
  function writeStandardMcpConfig(targetPath, workspaceRoot) {
202
196
  const existing = readJson(targetPath);
@@ -207,13 +201,13 @@ function writeStandardMcpConfig(targetPath, workspaceRoot) {
207
201
  }
208
202
  function writeOpencodeConfig(targetPath, workspaceRoot) {
209
203
  const existing = readJson(targetPath);
204
+ const mcpConfig = buildStandardMcpServerConfig(workspaceRoot);
210
205
  const server = {
211
206
  type: 'local',
212
- command: 'npx',
213
- args: ['-y', 'nexus-prime', 'mcp'],
207
+ command: mcpConfig.command,
208
+ args: mcpConfig.args,
214
209
  environment: {
215
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
216
- ...(workspaceRoot ? { NEXUS_WORKSPACE_ROOT: workspaceRoot } : {}),
210
+ ...mcpConfig.env,
217
211
  }
218
212
  };
219
213
  existing.mcp = existing.mcp ?? {};
@@ -251,21 +245,19 @@ function mergeCodexAgentsContent(existingContent, content) {
251
245
  const endIndex = existing.indexOf(CODEX_MANAGED_END);
252
246
  if (startIndex >= 0 && endIndex > startIndex) {
253
247
  const before = existing.slice(0, startIndex).trimEnd();
254
- const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trimStart();
248
+ const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trim();
255
249
  return [
256
250
  before,
257
251
  before ? '' : undefined,
258
252
  managedBlock,
259
253
  after ? '' : undefined,
260
254
  after,
261
- '',
262
255
  ].filter((value) => value !== undefined).join('\n');
263
256
  }
264
257
  return [
265
258
  existing.trimEnd(),
266
259
  '',
267
260
  managedBlock,
268
- '',
269
261
  ].join('\n');
270
262
  }
271
263
  function hasCurrentCodexManagedBlock(targetPath, content) {
@@ -301,17 +293,16 @@ function mergeClaudeContent(existingContent, content) {
301
293
  const endIndex = existing.indexOf(CLAUDE_MANAGED_END);
302
294
  if (startIndex >= 0 && endIndex > startIndex) {
303
295
  const before = existing.slice(0, startIndex).trimEnd();
304
- const after = existing.slice(endIndex + CLAUDE_MANAGED_END.length).trimStart();
296
+ const after = existing.slice(endIndex + CLAUDE_MANAGED_END.length).trim();
305
297
  return [
306
298
  before,
307
299
  before ? '' : undefined,
308
300
  managedBlock,
309
301
  after ? '' : undefined,
310
302
  after,
311
- '',
312
303
  ].filter((value) => value !== undefined).join('\n');
313
304
  }
314
- return [existing.trimEnd(), '', managedBlock, ''].join('\n');
305
+ return [existing.trimEnd(), '', managedBlock].join('\n');
315
306
  }
316
307
  function hasCurrentClaudeManagedBlock(targetPath, content) {
317
308
  if (!existsSync(targetPath))
@@ -346,17 +337,16 @@ function mergeClinerules(existingContent, content) {
346
337
  const endIndex = existing.indexOf(CLINE_MANAGED_END);
347
338
  if (startIndex >= 0 && endIndex > startIndex) {
348
339
  const before = existing.slice(0, startIndex).trimEnd();
349
- const after = existing.slice(endIndex + CLINE_MANAGED_END.length).trimStart();
340
+ const after = existing.slice(endIndex + CLINE_MANAGED_END.length).trim();
350
341
  return [
351
342
  before,
352
343
  before ? '' : undefined,
353
344
  managedBlock,
354
345
  after ? '' : undefined,
355
346
  after,
356
- '',
357
347
  ].filter((value) => value !== undefined).join('\n');
358
348
  }
359
- return [existing.trimEnd(), '', managedBlock, ''].join('\n');
349
+ return [existing.trimEnd(), '', managedBlock].join('\n');
360
350
  }
361
351
  function hasCurrentClineManagedBlock(targetPath, content) {
362
352
  if (!existsSync(targetPath))
@@ -591,12 +581,10 @@ function hasExpectedConfig(definition) {
591
581
  const parsed = JSON.parse(readFileSync(definition.configPath, 'utf8'));
592
582
  if (definition.id === 'opencode') {
593
583
  const server = parsed?.mcp?.['nexus-prime'];
594
- return Boolean(server
595
- && server?.environment?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
584
+ return Boolean(server && isStableNexusMcpServerConfig(server, 'environment'));
596
585
  }
597
586
  const server = parsed?.mcpServers?.['nexus-prime'];
598
- return Boolean(server
599
- && server?.env?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
587
+ return Boolean(server && isStableNexusMcpServerConfig(server));
600
588
  }
601
589
  catch {
602
590
  return false;
@@ -825,7 +813,8 @@ program
825
813
  const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
826
814
  const caller = detectCallerIDE();
827
815
  const marker = readSetupMarker();
828
- if (caller) {
816
+ const autoConfigEnabled = process.env.NEXUS_MCP_AUTO_CONFIG === '1';
817
+ if (caller && autoConfigEnabled) {
829
818
  const isConfigured = marker?.configuredIDEs?.includes(caller) ?? false;
830
819
  const storedHash = marker?.writtenHash?.[caller];
831
820
  if (isConfigured && storedHash !== undefined) {
@@ -879,6 +868,10 @@ program
879
868
  }
880
869
  }
881
870
  // Try daemon-backed proxy first unless --standalone is passed
871
+ const parsedDaemonTimeout = Number(process.env.NEXUS_MCP_DAEMON_TIMEOUT_MS ?? 5000);
872
+ const mcpDaemonTimeoutMs = Number.isFinite(parsedDaemonTimeout)
873
+ ? Math.max(1000, Math.min(parsedDaemonTimeout, 15000))
874
+ : 5000;
882
875
  if (!options.standalone) {
883
876
  try {
884
877
  const status = await getDaemonStatus(workspaceContext);
@@ -886,6 +879,7 @@ program
886
879
  console.error('Connecting to Nexus Prime daemon...');
887
880
  await startDaemonBackedMcpProxy(workspaceContext, {
888
881
  entrypoint: getCliEntrypoint(),
882
+ timeoutMs: mcpDaemonTimeoutMs,
889
883
  });
890
884
  return;
891
885
  }
@@ -900,9 +894,11 @@ program
900
894
  try {
901
895
  await ensureDaemonReady(workspaceContext, {
902
896
  entrypoint: getCliEntrypoint(),
897
+ timeoutMs: mcpDaemonTimeoutMs,
903
898
  });
904
899
  await startDaemonBackedMcpProxy(workspaceContext, {
905
900
  entrypoint: getCliEntrypoint(),
901
+ timeoutMs: mcpDaemonTimeoutMs,
906
902
  });
907
903
  return;
908
904
  }
@@ -1,4 +1,5 @@
1
1
  import type { WorkspaceContext } from '../engines/workspace-resolver.js';
2
2
  export declare function startDaemonBackedMcpProxy(workspace: WorkspaceContext, options?: {
3
3
  entrypoint?: string;
4
+ timeoutMs?: number;
4
5
  }): Promise<void>;
@@ -3,12 +3,15 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { daemonRequest, ensureDaemonReady, } from './client.js';
6
- async function withDaemonRetry(workspace, daemonRef, entrypoint, operation) {
6
+ async function withDaemonRetry(workspace, daemonRef, options, operation) {
7
7
  try {
8
8
  return await operation(daemonRef.current);
9
9
  }
10
10
  catch {
11
- daemonRef.current = await ensureDaemonReady(workspace, { entrypoint });
11
+ daemonRef.current = await ensureDaemonReady(workspace, {
12
+ entrypoint: options.entrypoint,
13
+ timeoutMs: options.timeoutMs,
14
+ });
12
15
  return operation(daemonRef.current);
13
16
  }
14
17
  }
@@ -16,18 +19,19 @@ export async function startDaemonBackedMcpProxy(workspace, options = {}) {
16
19
  const daemonRef = {
17
20
  current: await ensureDaemonReady(workspace, {
18
21
  entrypoint: options.entrypoint,
22
+ timeoutMs: options.timeoutMs,
19
23
  }),
20
24
  };
21
25
  const sessionId = randomUUID();
22
26
  const server = new Server({ name: 'nexus-prime-mcp-proxy', version: '0.1.0' }, { capabilities: { tools: {} } });
23
27
  server.setRequestHandler(ListToolsRequestSchema, async () => {
24
- const payload = await withDaemonRetry(workspace, daemonRef, options.entrypoint, (record) => daemonRequest(record, '/rpc/list-tools', { sessionId }));
28
+ const payload = await withDaemonRetry(workspace, daemonRef, options, (record) => daemonRequest(record, '/rpc/list-tools', { sessionId }));
25
29
  return {
26
30
  tools: payload.tools,
27
31
  };
28
32
  });
29
33
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
30
- const payload = await withDaemonRetry(workspace, daemonRef, options.entrypoint, (record) => daemonRequest(record, '/rpc/call-tool', {
34
+ const payload = await withDaemonRetry(workspace, daemonRef, options, (record) => daemonRequest(record, '/rpc/call-tool', {
31
35
  sessionId,
32
36
  name: request.params?.name,
33
37
  arguments: request.params?.arguments ?? {},
@@ -9,21 +9,35 @@ import { CACHE_TTL, S, bus } from './state.js';
9
9
 
10
10
  const _cache = new Map();
11
11
 
12
+ function _scopedUrl(url) {
13
+ if (typeof url !== 'string' || !url.startsWith('/api/')) return url;
14
+ const runtimeId = S.scope?.runtimeId;
15
+ if (!runtimeId) return url;
16
+ try {
17
+ const scoped = new URL(url, window.location.origin);
18
+ if (!scoped.searchParams.has('runtimeId')) scoped.searchParams.set('runtimeId', runtimeId);
19
+ return scoped.pathname + scoped.search + scoped.hash;
20
+ } catch {
21
+ return url;
22
+ }
23
+ }
24
+
12
25
  /**
13
26
  * GET with SWR semantics: return cached data immediately (if within TTL),
14
27
  * then kick off a background refresh. Callers get the fast synchronous hit
15
28
  * for p50 <5ms; the next render cycle gets the fresh value.
16
29
  */
17
30
  export async function api(url, ttl = CACHE_TTL) {
18
- const hit = _cache.get(url);
31
+ const requestUrl = _scopedUrl(url);
32
+ const hit = _cache.get(requestUrl);
19
33
  const fresh = !hit || (Date.now() - hit.ts >= ttl);
20
34
 
21
35
  // Return stale data synchronously while refresh fires in background.
22
- const refreshPromise = fresh ? _fetch(url) : Promise.resolve(hit.data);
36
+ const refreshPromise = fresh ? _fetch(requestUrl) : Promise.resolve(hit.data);
23
37
 
24
38
  if (hit && !fresh) {
25
39
  // Background refresh — don't await, just schedule.
26
- _fetch(url).catch(() => {});
40
+ _fetch(requestUrl).catch(() => {});
27
41
  }
28
42
 
29
43
  return refreshPromise;
@@ -52,9 +66,10 @@ async function _fetch(url) {
52
66
  */
53
67
  export async function post(url, body, opts = {}) {
54
68
  const { optimistic } = opts;
69
+ const requestUrl = _scopedUrl(url);
55
70
  // If an optimistic record is provided, return it right away and let the
56
71
  // caller reconcile once the real response arrives.
57
- const req = fetch(url, {
72
+ const req = fetch(requestUrl, {
58
73
  method: 'POST',
59
74
  headers: { 'Content-Type': 'application/json' },
60
75
  body: JSON.stringify(body),
@@ -77,7 +92,7 @@ export async function post(url, body, opts = {}) {
77
92
  if (optimistic !== undefined) {
78
93
  // Return immediately with optimistic data; the caller can await the
79
94
  // real result separately if it needs to reconcile.
80
- req.then((real) => { if (!real.ok) bus.emit('post:optimistic-fail', { url, error: real.error }); });
95
+ req.then((real) => { if (!real.ok) bus.emit('post:optimistic-fail', { url: requestUrl, error: real.error }); });
81
96
  return { ok: true, data: optimistic, error: null, status: 202, realResponse: req };
82
97
  }
83
98
  return req;
@@ -86,6 +101,8 @@ export async function post(url, body, opts = {}) {
86
101
  /** Invalidate a cached URL so the next api() call fetches fresh. */
87
102
  export function bustCache(url) {
88
103
  _cache.delete(url);
104
+ const scoped = _scopedUrl(url);
105
+ if (scoped !== url) _cache.delete(scoped);
89
106
  }
90
107
 
91
108
  /** Clear the entire cache (e.g. after an SSE reconnect). */
@@ -122,6 +139,15 @@ export function notifyNotReady(payloads) {
122
139
  for (const p of payloads) {
123
140
  if (!p || typeof p !== 'object' || !p.notReady) continue;
124
141
  const code = String(p.pillar || 'pillar') + '-not-ready';
125
- pushSystemError(code, p.reason || 'runtime not attached', undefined);
142
+ const reason = String(p.reason || '');
143
+ const optionalIdleRuntime = /not attached|not initiali[sz]ed|deferred|lazy/i.test(reason)
144
+ && !p.error
145
+ && !p.fatal
146
+ && !p.severity;
147
+ if (optionalIdleRuntime) {
148
+ clearSystemError(code);
149
+ continue;
150
+ }
151
+ pushSystemError(code, reason || 'runtime not attached', undefined);
126
152
  }
127
153
  }
@@ -185,6 +185,10 @@ if(location.protocol==='file:'){
185
185
  <div class="shd">Promotion history</div>
186
186
  <div class="card" style="margin-bottom:14px"><div id="mem-promotion-ticker" class="mem-promotion-ticker"></div></div>
187
187
 
188
+ <div class="memory-toolbar">
189
+ <button class="btn btn-sm" id="mem-graph-max-btn">Maximize graph</button>
190
+ <button class="btn btn-sm" id="mem-browse-btn">Browse memories</button>
191
+ </div>
188
192
  <div id="graph-container" class="card">
189
193
  <svg id="graph-svg"></svg>
190
194
  <div class="graph-legend">
@@ -208,6 +212,7 @@ if(location.protocol==='file:'){
208
212
  <div class="card">
209
213
  <div class="mem-search-row">
210
214
  <input id="mem-search" type="text" placeholder="Search memories…" aria-label="Search memories">
215
+ <button class="btn btn-sm" id="mem-list-browse-btn">Browse</button>
211
216
  </div>
212
217
  <div id="mem-list"></div>
213
218
  </div>
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { S, bus } from './state.js';
7
- import { api, post, bustCache } from './api.js';
7
+ import { api, bustCache, clearCache } from './api.js';
8
8
  import { connect as connectSSE,
9
9
  setOnEvent } from './sse.js';
10
10
  import { register as navRegister,
@@ -30,7 +30,9 @@ const esc = s => s==null?'':String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;')
30
30
  let memoryReloadTimer = null;
31
31
 
32
32
  function _memoryApiUrl() {
33
- return S.workspace?.repoName ? '/api/memory?repo='+encodeURIComponent(S.workspace.repoName) : '/api/memory';
33
+ const params = new URLSearchParams({ includeInactive: 'true', limit: '100' });
34
+ if (S.workspace?.repoName) params.set('repo', S.workspace.repoName);
35
+ return '/api/memory?' + params.toString();
34
36
  }
35
37
 
36
38
  function _refreshMemorySoon() {
@@ -170,25 +172,57 @@ function _fmtBytes(n) {
170
172
  }
171
173
 
172
174
  async function loadProjects() {
173
- const d = await api('/api/memory/projects', 30000);
174
- S.projects = Array.isArray(d) ? d : (d?.projects||[]);
175
+ const [projectsD, runtimesD] = await Promise.all([
176
+ api('/api/memory/projects', 30000),
177
+ api('/api/runtimes', 10000),
178
+ ]);
179
+ S.projects = Array.isArray(projectsD) ? projectsD : (projectsD?.projects||[]);
180
+ S.runtimes = Array.isArray(runtimesD) ? runtimesD : (runtimesD?.runtimes||[]);
181
+ _selectDefaultRuntime();
175
182
  _renderProjectSelectors();
176
183
  }
177
184
 
178
185
  async function loadCompanies() {
179
186
  const d = await api('/api/workforce/companies', 30000);
180
187
  const companies = Array.isArray(d?.companies) ? d.companies : [];
181
- const sel = $('ctx-project'); if (!sel) return;
182
- if (companies.length === 0) return;
183
- sel.innerHTML = '<option value="">All companies</option>' +
184
- companies.map(c=>`<option value="${esc(c)}">${esc(c)}</option>`).join('');
185
- sel.onchange = () => { S.company = sel.value || undefined; bustCache('/api/workforce/kanban'); bustCache('/api/workforce/workers'); bustCache('/api/workforce/jobs'); };
188
+ S.companies = companies;
186
189
  }
187
190
 
188
191
  function _renderProjectSelectors() {
189
192
  const sel = $('ctx-project'); if (!sel) return;
190
- sel.innerHTML = '<option value="">All projects</option>' +
191
- S.projects.map(p=>`<option value="${esc(p.id||p.projectId)}">${esc(p.name||p.id||'Project')}</option>`).join('');
193
+ const runtimeOptions = (S.runtimes||[]).map(r => {
194
+ const id = r.runtimeId || '';
195
+ const repo = r.repoIdentity || {};
196
+ const label = repo.repoName || repo.repoRoot || id || 'runtime';
197
+ const suffix = r.lastHeartbeatAt ? ' · runtime' : '';
198
+ return `<option value="runtime:${esc(id)}">${esc(label + suffix)}</option>`;
199
+ }).join('');
200
+ const projectOptions = S.projects.map(p=>`<option value="project:${esc(p.id||p.projectId)}">${esc(p.name||p.id||'Project')}</option>`).join('');
201
+ sel.innerHTML = '<option value="">All projects</option>' + runtimeOptions + projectOptions;
202
+ if (S.scope.runtimeId) sel.value = `runtime:${S.scope.runtimeId}`;
203
+ else if (S.scope.projectId) sel.value = `project:${S.scope.projectId}`;
204
+ }
205
+
206
+ function _selectDefaultRuntime() {
207
+ if (S.scope.runtimeId || !(S.runtimes||[]).length) return;
208
+ const current = S.workspace?.repoRoot || S.workspace?.workspaceRoot || '';
209
+ const candidates = (S.runtimes||[]).filter(r => r?.runtimeId);
210
+ const exact = current ? candidates.find(r => r.repoIdentity?.repoRoot === current) : null;
211
+ const currentLooksLikeHome = exact
212
+ && !exact.repoIdentity?.remoteUrl
213
+ && String(exact.repoIdentity?.repoRoot || '').split('/').filter(Boolean).length <= 2
214
+ && candidates.length > 1;
215
+ const remoteRepo = candidates.find(r => r.repoIdentity?.remoteUrl && r.repoIdentity?.repoName !== 'brew');
216
+ const deepestRepo = [...candidates].sort((a, b) => {
217
+ const depthA = String(a.repoIdentity?.repoRoot || '').split('/').filter(Boolean).length;
218
+ const depthB = String(b.repoIdentity?.repoRoot || '').split('/').filter(Boolean).length;
219
+ return depthB - depthA;
220
+ }).find(r => {
221
+ const root = r.repoIdentity?.repoRoot || '';
222
+ return root && root !== '/';
223
+ });
224
+ const selected = (exact && !currentLooksLikeHome ? exact : null) || remoteRepo || deepestRepo || candidates[0];
225
+ S.scope.runtimeId = selected?.runtimeId || null;
192
226
  }
193
227
 
194
228
  /* ─────────────────── Header / ticker ─────────────────── */
@@ -233,9 +267,18 @@ function _attachHandlers() {
233
267
  Memory.init();
234
268
 
235
269
  // Context selectors
236
- $('ctx-project')?.addEventListener('change', e => {
237
- S.scope.projectId = e.target.value || null;
238
- const mp = $('mem-project-filter'); if (mp) mp.value = e.target.value;
270
+ $('ctx-project')?.addEventListener('change', async e => {
271
+ const value = e.target.value || '';
272
+ if (value.startsWith('runtime:')) {
273
+ S.scope.runtimeId = value.slice('runtime:'.length) || null;
274
+ S.scope.projectId = null;
275
+ clearCache();
276
+ await loadWorkspace();
277
+ await loadRepoTree();
278
+ } else {
279
+ S.scope.projectId = value.startsWith('project:') ? value.slice('project:'.length) : (value || null);
280
+ }
281
+ const mp = $('mem-project-filter'); if (mp) mp.value = S.scope.projectId || '';
239
282
  _reloadTab(S.tab);
240
283
  });
241
284
  $('ctx-agent')?.addEventListener('change', e => {
@@ -274,7 +317,8 @@ async function _sendChat() {
274
317
  const uEl=document.createElement('div'); uEl.className='chat-msg user'; uEl.textContent=text; msgs.appendChild(uEl); msgs.scrollTop=msgs.scrollHeight;
275
318
  const sendBtn = $('chat-send-btn'); if (sendBtn) sendBtn.disabled=true;
276
319
  try {
277
- const res = await fetch('/api/runtime/execute',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ action:'chat', message:text, projectId:S.scope?.projectId }) }).then(r=>r.json()).catch(()=>null);
320
+ const qs = S.scope?.runtimeId ? `?runtimeId=${encodeURIComponent(S.scope.runtimeId)}` : '';
321
+ const res = await fetch('/api/runtime/execute'+qs,{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ action:'chat', goal:text, message:text, projectId:S.scope?.projectId, runtimeId:S.scope?.runtimeId }) }).then(r=>r.json()).catch(()=>null);
278
322
  const aEl=document.createElement('div'); aEl.className='chat-msg agent'; aEl.textContent=res?.result||res?.message||res?.output||'Command queued.'; msgs.appendChild(aEl);
279
323
  } catch { const aEl=document.createElement('div'); aEl.className='chat-msg agent'; aEl.textContent='Could not reach agent — is the server running?'; msgs.appendChild(aEl); }
280
324
  if (sendBtn) sendBtn.disabled=false;
@@ -292,11 +336,12 @@ async function bootstrap() {
292
336
  _attachHandlers();
293
337
  _renderHeader(false);
294
338
 
295
- // Fast first paint: board + workspace + projects + companies + repo-tree + license
296
- const [, , , , , lic] = await Promise.all([
339
+ await loadWorkspace();
340
+ await loadProjects();
341
+
342
+ // Fast first paint: board + companies + repo-tree + license
343
+ const [, , , lic] = await Promise.all([
297
344
  Board.load(),
298
- loadWorkspace(),
299
- loadProjects(),
300
345
  loadCompanies(),
301
346
  loadRepoTree(),
302
347
  api('/api/license', 60000),
@@ -24,7 +24,7 @@ export const S = {
24
24
  startTime: Date.now(),
25
25
 
26
26
  // Context scope
27
- scope: { projectId: null, agentId: null },
27
+ scope: { projectId: null, agentId: null, runtimeId: null },
28
28
  projects: [],
29
29
  workspace: null,
30
30
 
@@ -9,7 +9,16 @@
9
9
  background: var(--bg-elevated); border: 1px solid var(--border);
10
10
  border-radius: var(--radius); overflow: hidden; position: relative; margin-bottom: 12px;
11
11
  }
12
+ #graph-container.graph-expanded {
13
+ position: fixed; z-index: 60;
14
+ top: 56px; right: 24px; bottom: 24px; left: 360px;
15
+ height: auto; min-height: 0; margin-bottom: 0;
16
+ box-shadow: 0 0 0 1px rgba(0,255,136,0.18), 0 24px 80px rgba(0,0,0,0.68);
17
+ }
12
18
  #graph-svg { width: 100%; height: 100%; }
19
+ .memory-toolbar {
20
+ display: flex; justify-content: flex-end; gap: 8px; margin: -2px 0 8px;
21
+ }
13
22
  .graph-legend {
14
23
  position: absolute; top: 10px; right: 10px;
15
24
  display: flex; flex-direction: column; gap: 4px;
@@ -47,7 +56,7 @@
47
56
  display: flex; align-items: center; gap: 8px;
48
57
  padding: 8px 12px; border-bottom: 1px solid var(--border);
49
58
  }
50
- .mem-search-row input { flex: 1; border: none; outline: none; font-size: 0.78rem; color: var(--text-main); }
59
+ .mem-search-row input { flex: 1; border: none; outline: none; font-size: 0.78rem; color: var(--text-main); min-width: 0; }
51
60
  .mem-search-row input::placeholder { color: var(--text-dim); }
52
61
  #mem-list { max-height: 290px; overflow-y: auto; }
53
62
  .mem-item {
@@ -63,6 +72,11 @@
63
72
  .mem-title { font-size: 0.78rem; color: var(--text-main); line-height: 1.4; }
64
73
  .mem-meta { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-dim); margin-top: 2px; }
65
74
 
75
+ @media (max-width: 900px) {
76
+ #graph-container.graph-expanded { left: 16px; right: 16px; top: 56px; bottom: 16px; }
77
+ .memory-bottom { grid-template-columns: 1fr; }
78
+ }
79
+
66
80
  /* ── Memory Pyramid (widget container) ── */
67
81
  .pyramid-wrap {
68
82
  background: var(--bg-elevated); border: 1px solid var(--border);
@@ -45,9 +45,9 @@ export function render(fed, wards, escs) {
45
45
  const container = $('federation-container');
46
46
  if (!container) return;
47
47
 
48
- const peers = Array.isArray(fed.peers) ? fed.peers : [];
48
+ const peers = Array.isArray(fed.peers) ? fed.peers : (Array.isArray(fed.knownPeers) ? fed.knownPeers : []);
49
49
  const relay = fed.relay ?? null;
50
- const relayMode = fed.relayMode ?? 'degraded';
50
+ const relayMode = fed.relayMode ?? fed.relay?.mode ?? 'degraded';
51
51
 
52
52
  const dispatch = wards.dispatch ?? null;
53
53
  const worklist = Array.isArray(wards.worklist) ? wards.worklist : [];
@@ -65,13 +65,13 @@ export function render(fed, wards, escs) {
65
65
  </div>
66
66
  ${relay ? `
67
67
  <div class="trust-posture-body">
68
- ${_kv('Relay host', relay.host ?? '')}
68
+ ${_kv('Relay host', relay.host ?? (relay.configured ? 'configured' : 'local-only'))}
69
69
  ${_kv('Relay port', relay.port ?? '—')}
70
- ${_kv('Transport', relay.transport ?? '')}
70
+ ${_kv('Transport', relay.transport ?? 'local')}
71
71
  ${_kv('Connected', relay.connected ? 'yes' : 'no')}
72
72
  ${_kv('Latency', relay.latencyMs != null ? relay.latencyMs + ' ms' : '—')}
73
73
  </div>
74
- ` : `<div class="empty-sub">No relay configured.</div>`}
74
+ ` : `<div class="empty-sub">Local-only mode. Add a relay when you want cross-machine federation.</div>`}
75
75
  </div>
76
76
 
77
77
  <!-- Peer list card -->
@@ -101,7 +101,7 @@ export function render(fed, wards, escs) {
101
101
  ${_kv('Limit', dispatch.limit ?? '—')}
102
102
  ${_kv('Throttled', dispatch.throttled ? 'yes' : 'no')}
103
103
  </div>
104
- ` : `<div class="empty-sub">Architects runtime not initialised.</div>`}
104
+ ` : `<div class="empty-sub">Architects dispatch is idle. Worklists appear when a mission attaches the Architects runtime.</div>`}
105
105
  </div>
106
106
 
107
107
  <!-- Active worklist card -->
@@ -156,17 +156,17 @@ function _kv(label, value) {
156
156
  }
157
157
 
158
158
  function _peerRow(p) {
159
- const online = p.status === 'online' || p.connected === true;
159
+ const online = p.status === 'online' || p.health === 'online' || p.connected === true || p.active === true;
160
160
  return `
161
161
  <div class="trust-event-row">
162
162
  <div class="trust-event-main">
163
163
  <span class="chip ${online ? 'chip-ok' : 'chip-muted'}">${online ? 'online' : 'offline'}</span>
164
- <span class="trust-event-type">${esc(p.name ?? p.id ?? 'peer')}</span>
164
+ <span class="trust-event-type">${esc(p.displayName ?? p.name ?? p.peerId ?? p.id ?? 'peer')}</span>
165
165
  ${p.host ? `<span style="font-family:var(--font-mono);font-size:var(--caption);color:var(--text-muted)">${esc(p.host)}</span>` : ''}
166
166
  </div>
167
167
  <div class="trust-event-meta">
168
168
  ${p.latencyMs != null ? `<span style="font-family:var(--font-mono);font-size:var(--caption);color:var(--text-muted)">${p.latencyMs}ms</span>` : ''}
169
- ${p.lastSeenAt ? `<span style="color:var(--text-dim);font-size:var(--caption)">${fmtTs(p.lastSeenAt)}</span>` : ''}
169
+ ${p.lastSeenAt || p.lastHeartbeat ? `<span style="color:var(--text-dim);font-size:var(--caption)">${fmtTs(p.lastSeenAt || p.lastHeartbeat)}</span>` : ''}
170
170
  </div>
171
171
  </div>`;
172
172
  }
@@ -50,22 +50,36 @@ export async function load() {
50
50
  if (!container) return;
51
51
  container.innerHTML = `<div class="empty"><div class="empty-title">Loading proposals…</div></div>`;
52
52
 
53
- const data = await api('/api/governance/darwin', 10_000);
53
+ const [data, health] = await Promise.all([
54
+ api('/api/governance/darwin', 10_000),
55
+ api('/api/health', 15_000),
56
+ ]);
54
57
  const cycles = Array.isArray(data?.cycles) ? data.cycles : [];
55
58
 
56
- render(cycles);
59
+ render(cycles, health);
57
60
  }
58
61
 
59
- export function render(cycles) {
62
+ export function render(cycles, health = null) {
60
63
  const container = $('governance-container');
61
64
  if (!container) return;
62
65
 
63
66
  if (!cycles.length) {
64
67
  container.innerHTML = `
65
- <div class="empty">
66
- <div class="empty-title">No Darwin proposals yet</div>
67
- <div class="empty-sub">Auto-propose generates after nexus_orchestrate when <code>NEXUS_SELF_IMPROVE=1</code>.</div>
68
+ <div class="trust-grid">
69
+ <div class="card trust-card">
70
+ <div class="trust-card-hd"><span class="trust-card-title">Darwin auto-propose</span><span class="chip chip-muted">idle</span></div>
71
+ <div class="trust-posture-body">
72
+ <div class="trust-posture-row"><span>Proposals</span><strong>0</strong></div>
73
+ <div class="trust-posture-row"><span>Trigger</span><strong>nexus_orchestrate</strong></div>
74
+ <div class="trust-posture-row"><span>Self-improve</span><strong>${esc(health?.runtime?.selfImprove ? 'on' : 'off')}</strong></div>
75
+ </div>
76
+ </div>
77
+ <div class="card trust-card">
78
+ <div class="trust-card-hd"><span class="trust-card-title">Consensus guard</span><span class="chip chip-ok">ready</span></div>
79
+ <div class="empty-sub">Live Byzantine votes and review decisions appear here when proposals are created.</div>
80
+ </div>
68
81
  </div>`;
82
+ _renderByzantineSection();
69
83
  return;
70
84
  }
71
85
 
@@ -112,6 +126,7 @@ export function render(cycles) {
112
126
  });
113
127
  });
114
128
  });
129
+ _renderByzantineSection();
115
130
  }
116
131
 
117
132
  function cycleCard(c, isPending) {
@@ -30,15 +30,20 @@ function timeAgo(ts) {
30
30
 
31
31
  let _activeTier = null;
32
32
 
33
+ function memText(m) {
34
+ return m.title || m.excerpt || m.content || m.summary || m.id || '';
35
+ }
36
+
33
37
  /* ── Data loader ──
34
38
  * `allSettled` is deliberate: if the topology endpoint fails or times out,
35
39
  * the memory list + health + RAG collections should still render. Previous
36
40
  * behaviour (`Promise.all`) made a single failed call hide the whole section.
37
41
  */
38
42
  export async function load() {
39
- const repoParam = S.workspace?.repoName ? '?repo='+encodeURIComponent(S.workspace.repoName) : '';
43
+ const params = new URLSearchParams({ includeInactive: 'true', limit: '100' });
44
+ if (S.workspace?.repoName) params.set('repo', S.workspace.repoName);
40
45
  const [memsR, healthR, ragsR, topoR] = await Promise.allSettled([
41
- api('/api/memory'+repoParam, 5000),
46
+ api('/api/memory?'+params.toString(), 5000),
42
47
  api('/api/memory/health', 5000),
43
48
  api('/api/rag/collections', 15000),
44
49
  api('/api/knowledge-topology', 30000),
@@ -71,10 +76,22 @@ function renderPyramidWidget() {
71
76
  const container=$('mem-pyramid-widget'); if (!container) return;
72
77
  const h=S.memHealth;
73
78
  if (!h) { container.innerHTML=''; return; }
79
+ const fallback = S.memories.reduce((acc, m) => {
80
+ const tier = m.tier || 'semantic';
81
+ if (tier === 'working' || tier === 'prefrontal') acc.prefrontal += 1;
82
+ else if (tier === 'episodic' || tier === 'hippocampus') acc.hippocampus += 1;
83
+ else acc.cortex += 1;
84
+ return acc;
85
+ }, { prefrontal: 0, hippocampus: 0, cortex: 0 });
86
+ const tierCounts = h.tierCounts || {};
87
+ const count = (fallbackValue, ...values) => {
88
+ const positive = values.find(v => Number(v) > 0);
89
+ return positive != null ? Number(positive) : fallbackValue;
90
+ };
74
91
  const counts = {
75
- prefrontal: h.working ?? h.prefrontal ?? 0,
76
- hippocampus: h.episodic ?? h.hippocampus ?? 0,
77
- cortex: h.semantic ?? h.cortex ?? 0,
92
+ prefrontal: count(fallback.prefrontal, h.working, h.prefrontal, tierCounts.prefrontal),
93
+ hippocampus: count(fallback.hippocampus, h.episodic, h.hippocampus, tierCounts.hippocampus),
94
+ cortex: count(fallback.cortex, h.semantic, h.cortex, tierCounts.cortex),
78
95
  };
79
96
  renderPyramid(container, counts, _activeTier, tier => {
80
97
  _activeTier = tier;
@@ -362,11 +379,11 @@ export function renderMemList() {
362
379
  const q=S.memQuery.toLowerCase();
363
380
  let mems=S.memories.filter(m=>{
364
381
  if (_activeTier) {
365
- const tierMap={prefrontal:'working',hippocampus:'episodic',cortex:'semantic'};
366
- if ((m.tier||'semantic')!==tierMap[_activeTier]) return false;
382
+ const tierMap={prefrontal:['working','prefrontal'],hippocampus:['episodic','hippocampus'],cortex:['semantic','cortex']};
383
+ if (!tierMap[_activeTier].includes(m.tier||'semantic')) return false;
367
384
  }
368
385
  if (!q) return true;
369
- return [(m.title||''),(m.content||''),(m.tags||[]).join(' ')].some(s=>s.toLowerCase().includes(q));
386
+ return [memText(m),(m.content||''),(m.excerpt||''),(m.tags||[]).join(' ')].some(s=>s.toLowerCase().includes(q));
370
387
  }).slice(0,60);
371
388
  if (!mems.length) {
372
389
  const repoHint=S.workspace?.repoName?` for <code>${esc(S.workspace.repoName)}</code>`:'';
@@ -376,22 +393,39 @@ export function renderMemList() {
376
393
  el.innerHTML=mems.map(m=>{
377
394
  const tier=m.tier||'semantic';
378
395
  const pri=m.priority!=null?`p:${Math.round(m.priority*100)}%`:'';
396
+ const state=m.state && m.state !== 'active' ? ` · ${m.state}` : '';
379
397
  const decay=m.entropy>0.6?'opacity:0.6':'';
380
398
  return `<div class="mem-item" style="${decay}" data-memid="${esc(m.id)}">
381
399
  <div class="tier-dot t-${esc(tier)}"></div>
382
400
  <div style="flex:1;min-width:0">
383
- <div class="mem-title">${esc(m.title||(m.content||'').substring(0,60)||m.id)}</div>
384
- <div class="mem-meta">${esc(tier)}${pri?' · '+pri:''}${m.createdAt?' · '+timeAgo(m.createdAt):''}</div>
401
+ <div class="mem-title">${esc(memText(m).substring(0,90))}</div>
402
+ <div class="mem-meta">${esc(tier)}${esc(state)}${pri?' · '+pri:''}${m.createdAt?' · '+timeAgo(m.createdAt):''}</div>
385
403
  </div>
386
404
  </div>`;
387
405
  }).join('');
388
406
  el.querySelectorAll('[data-memid]').forEach(item => {
389
407
  item.addEventListener('click', () => {
390
- const m=S.memories.find(x=>x.id===item.dataset.memid); if (m) _openMemDrawer({id:m.id,label:m.title||m.id,data:m});
408
+ const m=S.memories.find(x=>x.id===item.dataset.memid); if (m) _openMemDrawer({id:m.id,label:memText(m),data:m});
391
409
  });
392
410
  });
393
411
  }
394
412
 
413
+ function _browseMemories() {
414
+ const items = S.memories.slice(0, 120);
415
+ openDrawer({
416
+ title: `Memories (${items.length})`,
417
+ body: items.length ? `<div class="trust-event-list">
418
+ ${items.map(m => `<div class="trust-event-row" data-drawer-mem="${esc(m.id)}">
419
+ <div class="trust-event-main">
420
+ <span class="chip chip-muted">${esc(m.tier || 'semantic')}</span>
421
+ <span class="trust-event-type">${esc(memText(m).slice(0, 120))}</span>
422
+ </div>
423
+ <div class="trust-event-meta">${esc(m.state || 'active')}${m.createdAt ? ' · '+esc(timeAgo(m.createdAt)) : ''}</div>
424
+ </div>`).join('')}
425
+ </div>` : '<div class="empty-sub">No memories available for the selected runtime.</div>',
426
+ });
427
+ }
428
+
395
429
  function _openMemDrawer(node) {
396
430
  const m=node.data||node;
397
431
  const tags=(m.tags||[]).map(t=>`<span class="chip">${esc(t)}</span>`).join(' ');
@@ -404,12 +438,22 @@ function _openMemDrawer(node) {
404
438
  ['Entropy',m.entropy!=null?m.entropy.toFixed(3):'—']
405
439
  ])}</div>
406
440
  ${tags?`<div class="dsec"><div class="dsec-title">Tags</div><div class="dtags">${tags}</div></div>`:''}
407
- <div class="dsec"><div class="dsec-title">Content</div><div class="dcontent">${esc(m.content||m.title||'(no content)')}</div></div>` });
441
+ <div class="dsec"><div class="dsec-title">Content</div><div class="dcontent">${esc(m.content||m.excerpt||m.title||'(no content)')}</div></div>` });
408
442
  }
409
443
 
410
444
  /* ── Init ── */
411
445
  export function init() {
412
446
  $('mem-search')?.addEventListener('input', e => { S.memQuery=e.target.value; renderMemList(); });
447
+ $('mem-browse-btn')?.addEventListener('click', _browseMemories);
448
+ $('mem-list-browse-btn')?.addEventListener('click', _browseMemories);
449
+ $('mem-graph-max-btn')?.addEventListener('click', () => {
450
+ const c = $('graph-container');
451
+ const b = $('mem-graph-max-btn');
452
+ if (!c || !b) return;
453
+ const expanded = c.classList.toggle('graph-expanded');
454
+ b.textContent = expanded ? 'Restore graph' : 'Maximize graph';
455
+ renderGraph();
456
+ });
413
457
  document.querySelectorAll('.lane-btn').forEach(b => b.addEventListener('click', () => {
414
458
  S.memLane=b.dataset.lane;
415
459
  document.querySelectorAll('.lane-btn').forEach(x=>x.classList.toggle('active',x===b));
@@ -238,18 +238,22 @@ function _worktreesSection(health) {
238
238
  function _clientsSection(clients, primary) {
239
239
  if (!clients.length) return `<div class="empty-sub">No clients detected.</div>`;
240
240
  return `<div class="trust-event-list">
241
- ${clients.slice(0, 10).map(c => `
241
+ ${clients.slice(0, 10).map(c => {
242
+ const id = c.clientId ?? c.id ?? c.name ?? '';
243
+ const name = c.displayName ?? c.name ?? c.clientId ?? c.id ?? 'unknown';
244
+ const primaryId = primary?.clientId ?? primary?.id ?? primary?.name ?? '';
245
+ return `
242
246
  <div class="trust-event-row">
243
247
  <div class="trust-event-main">
244
- ${c.name === primary?.name ? `<span class="chip chip-ok">primary</span>` : `<span class="chip chip-muted">client</span>`}
245
- <span class="trust-event-type">${esc(c.name ?? c.id ?? 'unknown')}</span>
248
+ ${id === primaryId ? `<span class="chip chip-ok">primary</span>` : `<span class="chip chip-muted">client</span>`}
249
+ <span class="trust-event-type">${esc(name)}</span>
246
250
  ${c.version ? `<span style="font-family:var(--font-mono);font-size:var(--caption);color:var(--text-muted)">v${esc(c.version)}</span>` : ''}
247
251
  </div>
248
252
  <div class="trust-event-meta">
249
253
  ${c.sessions != null ? `<span style="font-size:var(--caption);color:var(--text-muted)">${c.sessions} session${c.sessions !== 1 ? 's' : ''}</span>` : ''}
250
254
  </div>
251
255
  </div>
252
- `).join('')}
256
+ `;}).join('')}
253
257
  </div>`;
254
258
  }
255
259
 
@@ -54,6 +54,7 @@ export const handleMemoryRoutes = async (ctx, req, res, url) => {
54
54
  const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
55
55
  const projectId = url.searchParams.get('projectId') ?? undefined;
56
56
  const includeHidden = url.searchParams.get('includeHidden') === 'true';
57
+ const includeInactive = url.searchParams.get('includeInactive') === 'true';
57
58
  // ?repo=<name> scopes results to memories tagged repo:<name> (written by auto-memory).
58
59
  // Defaults to the current workspace repo name when not supplied by the frontend.
59
60
  const repoNameRaw = url.searchParams.get('repo');
@@ -70,6 +71,7 @@ export const handleMemoryRoutes = async (ctx, req, res, url) => {
70
71
  workspaceId,
71
72
  projectId,
72
73
  includeHidden,
74
+ includeInactive,
73
75
  repoName,
74
76
  }));
75
77
  return true;
@@ -190,11 +190,12 @@ export const handleRuntimeRoutes = async (ctx, req, res, url) => {
190
190
  ctx.respondJson(res, { error: 'orchestrator-unavailable' }, 503);
191
191
  return true;
192
192
  }
193
- if (!body.goal || typeof body.goal !== 'string') {
193
+ const goal = typeof body.goal === 'string' ? body.goal : (typeof body.message === 'string' ? body.message : '');
194
+ if (!goal.trim()) {
194
195
  ctx.respondJson(res, { error: 'goal-required' }, 400);
195
196
  return true;
196
197
  }
197
- const run = await orchestrator.orchestrate(String(body.goal), body);
198
+ const run = await orchestrator.orchestrate(goal, { ...body, goal });
198
199
  nexusEventBus.emit('dashboard.action', {
199
200
  action: 'runtime.execute',
200
201
  status: run.state,
@@ -395,10 +395,7 @@ export class DashboardServer {
395
395
  }
396
396
  createRouteContext() {
397
397
  const runtimes = this.runtimeRegistry.list();
398
- const runtimeKeyFor = (url) => (url.searchParams.get('runtimeId')
399
- || this.getRuntime()?.getRuntimeId()
400
- || runtimes[0]?.runtimeId
401
- || '__default__');
398
+ const runtimeKeyFor = (url) => this.resolveRequestedRuntimeId(url, runtimes) || '__default__';
402
399
  const runtimeSnapshotCache = new Map();
403
400
  const usageSnapshotCache = new Map();
404
401
  const repoIdentityCache = new Map();
@@ -618,8 +615,19 @@ export class DashboardServer {
618
615
  return this.runtimeProvider?.();
619
616
  }
620
617
  resolveRequestedRuntimeId(url, runtimes = this.runtimeRegistry.list()) {
621
- return url.searchParams.get('runtimeId')
622
- || this.getRuntime()?.getRuntimeId()
618
+ const requested = url.searchParams.get('runtimeId');
619
+ if (requested)
620
+ return requested;
621
+ const currentRuntimeId = this.getRuntime()?.getRuntimeId();
622
+ const currentSnapshot = currentRuntimeId
623
+ ? (runtimes.find((runtime) => runtime.runtimeId === currentRuntimeId) ?? this.runtimeRegistry.read(currentRuntimeId))
624
+ : null;
625
+ const currentRepoRoot = currentSnapshot?.repoIdentity?.repoRoot;
626
+ if (currentRuntimeId && currentRepoRoot === this.repoRoot)
627
+ return currentRuntimeId;
628
+ const repoRuntime = runtimes.find((runtime) => runtime?.repoIdentity?.repoRoot === this.repoRoot);
629
+ return repoRuntime?.runtimeId
630
+ || currentRuntimeId
623
631
  || runtimes[0]?.runtimeId
624
632
  || null;
625
633
  }
@@ -836,7 +844,7 @@ export class DashboardServer {
836
844
  tag: options.tag,
837
845
  linkedType: options.linkedType,
838
846
  recencyMs: options.recencyMs,
839
- state: options.includeHidden || options.lane === 'inbox' ? undefined : 'active',
847
+ state: options.includeHidden || options.includeInactive || options.lane === 'inbox' ? undefined : 'active',
840
848
  lane: options.lane,
841
849
  repoId: options.repoId,
842
850
  workspaceId: options.workspaceId,
@@ -4,6 +4,7 @@ import { homedir } from 'os';
4
4
  import { dirname, join, resolve } from 'path';
5
5
  import { InstructionGateway } from './instruction-gateway.js';
6
6
  import { MemoryEngine } from './memory.js';
7
+ import { buildNexusMcpServerConfig, isStableNexusMcpServerConfig } from './mcp-entrypoint.js';
7
8
  import { resolveHomeCodexRoot, resolveWorkspaceContext } from './workspace-resolver.js';
8
9
  const WELCOME_MEMORY_MESSAGE = "Welcome to Nexus Prime! Nexus Prime is fully installed and tracking context for your workspace. Use '/nexus' to ask me to analyze the codebase, run agents, and automate tasks.";
9
10
  function buildWelcomeMemoryTags(workspaceStateKey) {
@@ -218,14 +219,7 @@ function readJson(targetPath) {
218
219
  }
219
220
  }
220
221
  function buildStandardMcpServerConfig(workspaceRoot) {
221
- return {
222
- command: 'npx',
223
- args: ['-y', 'nexus-prime', 'mcp'],
224
- env: {
225
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
226
- ...(workspaceRoot ? { NEXUS_WORKSPACE_ROOT: workspaceRoot } : {}),
227
- },
228
- };
222
+ return buildNexusMcpServerConfig(workspaceRoot);
229
223
  }
230
224
  function isAtlasDetected() {
231
225
  try {
@@ -298,10 +292,11 @@ function codexTomlHasAutonomousProfile(doc) {
298
292
  const envSection = getTomlSection(doc, 'mcp_servers.nexus-prime.env');
299
293
  if (!mcpSection)
300
294
  return false;
301
- const commandOk = /(?:^|\n)\s*command\s*=\s*"npx"\s*(?:\n|$)/.test(mcpSection);
302
- const argsOk = /(?:^|\n)\s*args\s*=\s*\[[^\]]*"nexus-prime"[^\]]*"mcp"[^\]]*\]\s*(?:\n|$)/s.test(mcpSection);
295
+ const commandOk = !/(?:^|\n)\s*command\s*=\s*"npx"\s*(?:\n|$)/.test(mcpSection);
296
+ const argsOk = /(?:^|\n)\s*args\s*=\s*\[[^\]]*"mcp"[^\]]*\]\s*(?:\n|$)/s.test(mcpSection);
303
297
  const envOk = Boolean(envSection && /(?:^|\n)\s*NEXUS_MCP_TOOL_PROFILE\s*=\s*"autonomous"\s*(?:\n|$)/.test(envSection));
304
- return commandOk && argsOk && envOk;
298
+ const noStartupRewrite = Boolean(envSection && /(?:^|\n)\s*NEXUS_MCP_AUTO_CONFIG\s*=\s*"0"\s*(?:\n|$)/.test(envSection));
299
+ return commandOk && argsOk && envOk && noStartupRewrite;
305
300
  }
306
301
  export function resolveCodexConfigPath() {
307
302
  const codexHome = resolveHomeCodexRoot();
@@ -314,13 +309,14 @@ export function resolveCodexConfigPath() {
314
309
  return tomlPath;
315
310
  }
316
311
  export function renderCodexMcpTomlConfig() {
312
+ const server = buildNexusMcpServerConfig();
317
313
  return [
318
314
  '[mcp_servers.nexus-prime]',
319
- 'command = "npx"',
320
- `args = ${formatTomlStringArray(['-y', 'nexus-prime', 'mcp'])}`,
315
+ `command = ${formatTomlString(server.command)}`,
316
+ `args = ${formatTomlStringArray(server.args)}`,
321
317
  '',
322
318
  '[mcp_servers.nexus-prime.env]',
323
- 'NEXUS_MCP_TOOL_PROFILE = "autonomous"',
319
+ ...Object.entries(server.env).map(([key, value]) => `${key} = ${formatTomlString(value)}`),
324
320
  ].join('\n');
325
321
  }
326
322
  export function writeCodexMcpConfig(targetPath) {
@@ -336,7 +332,7 @@ export function hasExpectedCodexConfig(targetPath) {
336
332
  const parsed = readJson(targetPath);
337
333
  const server = parsed?.mcpServers?.['nexus-prime'];
338
334
  return Boolean(server
339
- && server?.env?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
335
+ && isStableNexusMcpServerConfig(server));
340
336
  }
341
337
  try {
342
338
  return codexTomlHasAutonomousProfile(readFileSync(targetPath, 'utf8'));
@@ -358,13 +354,13 @@ function writeStandardMcpConfig(targetPath, workspaceRoot) {
358
354
  }
359
355
  function writeOpencodeConfig(targetPath, workspaceRoot) {
360
356
  const existing = readJson(targetPath);
357
+ const mcpConfig = buildStandardMcpServerConfig(workspaceRoot);
361
358
  const server = {
362
359
  type: 'local',
363
- command: 'npx',
364
- args: ['-y', 'nexus-prime', 'mcp'],
360
+ command: mcpConfig.command,
361
+ args: mcpConfig.args,
365
362
  environment: {
366
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
367
- ...(workspaceRoot ? { NEXUS_WORKSPACE_ROOT: workspaceRoot } : {}),
363
+ ...mcpConfig.env,
368
364
  },
369
365
  };
370
366
  existing.mcp = existing.mcp ?? {};
@@ -402,21 +398,19 @@ function mergeCodexAgentsContent(existingContent, content) {
402
398
  const endIndex = existing.indexOf(CODEX_MANAGED_END);
403
399
  if (startIndex >= 0 && endIndex > startIndex) {
404
400
  const before = existing.slice(0, startIndex).trimEnd();
405
- const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trimStart();
401
+ const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trim();
406
402
  return [
407
403
  before,
408
404
  before ? '' : undefined,
409
405
  managedBlock,
410
406
  after ? '' : undefined,
411
407
  after,
412
- '',
413
408
  ].filter((value) => value !== undefined).join('\n');
414
409
  }
415
410
  return [
416
411
  existing.trimEnd(),
417
412
  '',
418
413
  managedBlock,
419
- '',
420
414
  ].join('\n');
421
415
  }
422
416
  function hasCurrentCodexManagedBlock(targetPath, content) {
@@ -654,11 +648,11 @@ export function hasExpectedConfig(definition) {
654
648
  if (definition.id === 'opencode') {
655
649
  const server = parsed?.mcp?.['nexus-prime'];
656
650
  return Boolean(server
657
- && server?.environment?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
651
+ && isStableNexusMcpServerConfig(server, 'environment'));
658
652
  }
659
653
  const server = parsed?.mcpServers?.['nexus-prime'];
660
654
  return Boolean(server
661
- && server?.env?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
655
+ && isStableNexusMcpServerConfig(server));
662
656
  }
663
657
  catch {
664
658
  return false;
@@ -0,0 +1,12 @@
1
+ export interface NexusMcpServerConfig {
2
+ command: string;
3
+ args: string[];
4
+ env: Record<string, string>;
5
+ }
6
+ export declare function buildNexusMcpCommand(): {
7
+ command: string;
8
+ args: string[];
9
+ };
10
+ export declare function buildNexusMcpEnv(workspaceRoot?: string): Record<string, string>;
11
+ export declare function buildNexusMcpServerConfig(workspaceRoot?: string): NexusMcpServerConfig;
12
+ export declare function isStableNexusMcpServerConfig(server: any, envKey?: 'env' | 'environment'): boolean;
@@ -0,0 +1,33 @@
1
+ export function buildNexusMcpCommand() {
2
+ return {
3
+ command: 'nexus-prime',
4
+ args: ['mcp'],
5
+ };
6
+ }
7
+ export function buildNexusMcpEnv(workspaceRoot) {
8
+ return {
9
+ NEXUS_MCP_TOOL_PROFILE: 'autonomous',
10
+ NEXUS_MCP_AUTO_CONFIG: '0',
11
+ NEXUS_MCP_DAEMON_TIMEOUT_MS: '5000',
12
+ ...(workspaceRoot ? { NEXUS_WORKSPACE_ROOT: workspaceRoot } : {}),
13
+ };
14
+ }
15
+ export function buildNexusMcpServerConfig(workspaceRoot) {
16
+ const { command, args } = buildNexusMcpCommand();
17
+ return {
18
+ command,
19
+ args,
20
+ env: buildNexusMcpEnv(workspaceRoot),
21
+ };
22
+ }
23
+ export function isStableNexusMcpServerConfig(server, envKey = 'env') {
24
+ const env = server?.[envKey];
25
+ const args = Array.isArray(server?.args) ? server.args.map((arg) => String(arg)) : [];
26
+ return Boolean(server
27
+ && typeof server.command === 'string'
28
+ && server.command !== 'npx'
29
+ && args.includes('mcp')
30
+ && !args.includes('-y')
31
+ && env?.NEXUS_MCP_TOOL_PROFILE === 'autonomous'
32
+ && env?.NEXUS_MCP_AUTO_CONFIG === '0');
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.9.9",
3
+ "version": "7.9.10",
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",