nexus-prime 7.9.9 → 7.9.11

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 ?? {};
@@ -234,6 +228,9 @@ function renderCodexManagedBlock(content) {
234
228
  CODEX_MANAGED_END,
235
229
  ].join('\n');
236
230
  }
231
+ function ensureFinalNewline(content) {
232
+ return content.endsWith('\n') ? content : `${content}\n`;
233
+ }
237
234
  function mergeCodexAgentsContent(existingContent, content) {
238
235
  const managedBlock = renderCodexManagedBlock(content);
239
236
  const existing = existingContent ?? '';
@@ -251,22 +248,20 @@ function mergeCodexAgentsContent(existingContent, content) {
251
248
  const endIndex = existing.indexOf(CODEX_MANAGED_END);
252
249
  if (startIndex >= 0 && endIndex > startIndex) {
253
250
  const before = existing.slice(0, startIndex).trimEnd();
254
- const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trimStart();
255
- return [
251
+ const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trim();
252
+ return ensureFinalNewline([
256
253
  before,
257
254
  before ? '' : undefined,
258
255
  managedBlock,
259
256
  after ? '' : undefined,
260
257
  after,
261
- '',
262
- ].filter((value) => value !== undefined).join('\n');
258
+ ].filter((value) => value !== undefined).join('\n'));
263
259
  }
264
- return [
260
+ return ensureFinalNewline([
265
261
  existing.trimEnd(),
266
262
  '',
267
263
  managedBlock,
268
- '',
269
- ].join('\n');
264
+ ].join('\n'));
270
265
  }
271
266
  function hasCurrentCodexManagedBlock(targetPath, content) {
272
267
  if (!existsSync(targetPath))
@@ -301,17 +296,16 @@ function mergeClaudeContent(existingContent, content) {
301
296
  const endIndex = existing.indexOf(CLAUDE_MANAGED_END);
302
297
  if (startIndex >= 0 && endIndex > startIndex) {
303
298
  const before = existing.slice(0, startIndex).trimEnd();
304
- const after = existing.slice(endIndex + CLAUDE_MANAGED_END.length).trimStart();
305
- return [
299
+ const after = existing.slice(endIndex + CLAUDE_MANAGED_END.length).trim();
300
+ return ensureFinalNewline([
306
301
  before,
307
302
  before ? '' : undefined,
308
303
  managedBlock,
309
304
  after ? '' : undefined,
310
305
  after,
311
- '',
312
- ].filter((value) => value !== undefined).join('\n');
306
+ ].filter((value) => value !== undefined).join('\n'));
313
307
  }
314
- return [existing.trimEnd(), '', managedBlock, ''].join('\n');
308
+ return ensureFinalNewline([existing.trimEnd(), '', managedBlock].join('\n'));
315
309
  }
316
310
  function hasCurrentClaudeManagedBlock(targetPath, content) {
317
311
  if (!existsSync(targetPath))
@@ -346,17 +340,16 @@ function mergeClinerules(existingContent, content) {
346
340
  const endIndex = existing.indexOf(CLINE_MANAGED_END);
347
341
  if (startIndex >= 0 && endIndex > startIndex) {
348
342
  const before = existing.slice(0, startIndex).trimEnd();
349
- const after = existing.slice(endIndex + CLINE_MANAGED_END.length).trimStart();
350
- return [
343
+ const after = existing.slice(endIndex + CLINE_MANAGED_END.length).trim();
344
+ return ensureFinalNewline([
351
345
  before,
352
346
  before ? '' : undefined,
353
347
  managedBlock,
354
348
  after ? '' : undefined,
355
349
  after,
356
- '',
357
- ].filter((value) => value !== undefined).join('\n');
350
+ ].filter((value) => value !== undefined).join('\n'));
358
351
  }
359
- return [existing.trimEnd(), '', managedBlock, ''].join('\n');
352
+ return ensureFinalNewline([existing.trimEnd(), '', managedBlock].join('\n'));
360
353
  }
361
354
  function hasCurrentClineManagedBlock(targetPath, content) {
362
355
  if (!existsSync(targetPath))
@@ -591,12 +584,10 @@ function hasExpectedConfig(definition) {
591
584
  const parsed = JSON.parse(readFileSync(definition.configPath, 'utf8'));
592
585
  if (definition.id === 'opencode') {
593
586
  const server = parsed?.mcp?.['nexus-prime'];
594
- return Boolean(server
595
- && server?.environment?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
587
+ return Boolean(server && isStableNexusMcpServerConfig(server, 'environment'));
596
588
  }
597
589
  const server = parsed?.mcpServers?.['nexus-prime'];
598
- return Boolean(server
599
- && server?.env?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
590
+ return Boolean(server && isStableNexusMcpServerConfig(server));
600
591
  }
601
592
  catch {
602
593
  return false;
@@ -825,7 +816,8 @@ program
825
816
  const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
826
817
  const caller = detectCallerIDE();
827
818
  const marker = readSetupMarker();
828
- if (caller) {
819
+ const autoConfigEnabled = process.env.NEXUS_MCP_AUTO_CONFIG === '1';
820
+ if (caller && autoConfigEnabled) {
829
821
  const isConfigured = marker?.configuredIDEs?.includes(caller) ?? false;
830
822
  const storedHash = marker?.writtenHash?.[caller];
831
823
  if (isConfigured && storedHash !== undefined) {
@@ -879,6 +871,10 @@ program
879
871
  }
880
872
  }
881
873
  // Try daemon-backed proxy first unless --standalone is passed
874
+ const parsedDaemonTimeout = Number(process.env.NEXUS_MCP_DAEMON_TIMEOUT_MS ?? 5000);
875
+ const mcpDaemonTimeoutMs = Number.isFinite(parsedDaemonTimeout)
876
+ ? Math.max(1000, Math.min(parsedDaemonTimeout, 15000))
877
+ : 5000;
882
878
  if (!options.standalone) {
883
879
  try {
884
880
  const status = await getDaemonStatus(workspaceContext);
@@ -886,6 +882,7 @@ program
886
882
  console.error('Connecting to Nexus Prime daemon...');
887
883
  await startDaemonBackedMcpProxy(workspaceContext, {
888
884
  entrypoint: getCliEntrypoint(),
885
+ timeoutMs: mcpDaemonTimeoutMs,
889
886
  });
890
887
  return;
891
888
  }
@@ -900,9 +897,11 @@ program
900
897
  try {
901
898
  await ensureDaemonReady(workspaceContext, {
902
899
  entrypoint: getCliEntrypoint(),
900
+ timeoutMs: mcpDaemonTimeoutMs,
903
901
  });
904
902
  await startDaemonBackedMcpProxy(workspaceContext, {
905
903
  entrypoint: getCliEntrypoint(),
904
+ timeoutMs: mcpDaemonTimeoutMs,
906
905
  });
907
906
  return;
908
907
  }
@@ -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 ?? {};
@@ -385,6 +381,9 @@ function renderCodexManagedBlock(content) {
385
381
  CODEX_MANAGED_END,
386
382
  ].join('\n');
387
383
  }
384
+ function ensureFinalNewline(content) {
385
+ return content.endsWith('\n') ? content : `${content}\n`;
386
+ }
388
387
  function mergeCodexAgentsContent(existingContent, content) {
389
388
  const managedBlock = renderCodexManagedBlock(content);
390
389
  const existing = existingContent ?? '';
@@ -402,22 +401,20 @@ function mergeCodexAgentsContent(existingContent, content) {
402
401
  const endIndex = existing.indexOf(CODEX_MANAGED_END);
403
402
  if (startIndex >= 0 && endIndex > startIndex) {
404
403
  const before = existing.slice(0, startIndex).trimEnd();
405
- const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trimStart();
406
- return [
404
+ const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trim();
405
+ return ensureFinalNewline([
407
406
  before,
408
407
  before ? '' : undefined,
409
408
  managedBlock,
410
409
  after ? '' : undefined,
411
410
  after,
412
- '',
413
- ].filter((value) => value !== undefined).join('\n');
411
+ ].filter((value) => value !== undefined).join('\n'));
414
412
  }
415
- return [
413
+ return ensureFinalNewline([
416
414
  existing.trimEnd(),
417
415
  '',
418
416
  managedBlock,
419
- '',
420
- ].join('\n');
417
+ ].join('\n'));
421
418
  }
422
419
  function hasCurrentCodexManagedBlock(targetPath, content) {
423
420
  if (!existsSync(targetPath))
@@ -654,11 +651,11 @@ export function hasExpectedConfig(definition) {
654
651
  if (definition.id === 'opencode') {
655
652
  const server = parsed?.mcp?.['nexus-prime'];
656
653
  return Boolean(server
657
- && server?.environment?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
654
+ && isStableNexusMcpServerConfig(server, 'environment'));
658
655
  }
659
656
  const server = parsed?.mcpServers?.['nexus-prime'];
660
657
  return Boolean(server
661
- && server?.env?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
658
+ && isStableNexusMcpServerConfig(server));
662
659
  }
663
660
  catch {
664
661
  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.11",
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",