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.
- package/dist/agents/adapters/ide-compat.js +2 -8
- package/dist/cli/install-wizard.js +5 -14
- package/dist/cli.js +33 -34
- package/dist/daemon/proxy.d.ts +1 -0
- package/dist/daemon/proxy.js +8 -4
- package/dist/dashboard/app/api.js +32 -6
- package/dist/dashboard/app/index.html +5 -0
- package/dist/dashboard/app/main.js +64 -19
- package/dist/dashboard/app/state.js +1 -1
- package/dist/dashboard/app/styles/memory.css +15 -1
- package/dist/dashboard/app/views/federation.js +9 -9
- package/dist/dashboard/app/views/governance.js +21 -6
- package/dist/dashboard/app/views/memory.js +56 -12
- package/dist/dashboard/app/views/trust.js +8 -4
- package/dist/dashboard/routes/memory.js +2 -0
- package/dist/dashboard/routes/runtime.js +3 -2
- package/dist/dashboard/server.js +15 -7
- package/dist/engines/client-bootstrap.js +25 -28
- package/dist/engines/mcp-entrypoint.d.ts +12 -0
- package/dist/engines/mcp-entrypoint.js +33 -0
- package/package.json +1 -1
|
@@ -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:
|
|
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:
|
|
213
|
-
args:
|
|
207
|
+
command: mcpConfig.command,
|
|
208
|
+
args: mcpConfig.args,
|
|
214
209
|
environment: {
|
|
215
|
-
|
|
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).
|
|
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).
|
|
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
|
|
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).
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/daemon/proxy.d.ts
CHANGED
package/dist/daemon/proxy.js
CHANGED
|
@@ -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,
|
|
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, {
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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,'&').replace(/</g,'<')
|
|
|
30
30
|
let memoryReloadTimer = null;
|
|
31
31
|
|
|
32
32
|
function _memoryApiUrl() {
|
|
33
|
-
|
|
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
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
|
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
|
-
|
|
296
|
-
|
|
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),
|
|
@@ -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">
|
|
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
|
|
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
|
|
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="
|
|
66
|
-
<div class="
|
|
67
|
-
|
|
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
|
|
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'+
|
|
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
|
|
76
|
-
hippocampus: h.episodic
|
|
77
|
-
cortex: h.semantic
|
|
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')
|
|
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.
|
|
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(
|
|
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
|
|
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
|
-
${
|
|
245
|
-
<span class="trust-event-type">${esc(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
package/dist/dashboard/server.js
CHANGED
|
@@ -395,10 +395,7 @@ export class DashboardServer {
|
|
|
395
395
|
}
|
|
396
396
|
createRouteContext() {
|
|
397
397
|
const runtimes = this.runtimeRegistry.list();
|
|
398
|
-
const runtimeKeyFor = (url) => (url
|
|
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
|
-
|
|
622
|
-
|
|
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 =
|
|
302
|
-
const argsOk = /(?:^|\n)\s*args\s*=\s*\[[^\]]*"
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
`args = ${formatTomlStringArray(
|
|
315
|
+
`command = ${formatTomlString(server.command)}`,
|
|
316
|
+
`args = ${formatTomlStringArray(server.args)}`,
|
|
321
317
|
'',
|
|
322
318
|
'[mcp_servers.nexus-prime.env]',
|
|
323
|
-
|
|
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
|
|
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:
|
|
364
|
-
args:
|
|
360
|
+
command: mcpConfig.command,
|
|
361
|
+
args: mcpConfig.args,
|
|
365
362
|
environment: {
|
|
366
|
-
|
|
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).
|
|
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
|
|
654
|
+
&& isStableNexusMcpServerConfig(server, 'environment'));
|
|
658
655
|
}
|
|
659
656
|
const server = parsed?.mcpServers?.['nexus-prime'];
|
|
660
657
|
return Boolean(server
|
|
661
|
-
&& server
|
|
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.
|
|
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",
|