nexus-prime 7.9.8 → 7.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/agents/adapters/ide-compat.js +2 -8
  2. package/dist/agents/adapters/mcp/definitions.js +13 -0
  3. package/dist/agents/adapters/mcp/handlers/orchestration.d.ts +2 -0
  4. package/dist/agents/adapters/mcp/handlers/orchestration.js +34 -5
  5. package/dist/agents/adapters/mcp/handlers/runtime.js +36 -1
  6. package/dist/agents/adapters/mcp/helpers.js +1 -1
  7. package/dist/cli/install-wizard.js +5 -14
  8. package/dist/cli.js +22 -26
  9. package/dist/daemon/proxy.d.ts +1 -0
  10. package/dist/daemon/proxy.js +8 -4
  11. package/dist/dashboard/app/api.js +32 -6
  12. package/dist/dashboard/app/index.html +5 -0
  13. package/dist/dashboard/app/main.js +64 -19
  14. package/dist/dashboard/app/state.js +1 -1
  15. package/dist/dashboard/app/styles/memory.css +15 -1
  16. package/dist/dashboard/app/views/federation.js +9 -9
  17. package/dist/dashboard/app/views/governance.js +21 -6
  18. package/dist/dashboard/app/views/memory.js +56 -12
  19. package/dist/dashboard/app/views/trust.js +8 -4
  20. package/dist/dashboard/routes/memory.js +2 -0
  21. package/dist/dashboard/routes/runtime.js +12 -2
  22. package/dist/dashboard/selectors/assets-selector.js +2 -0
  23. package/dist/dashboard/server.js +15 -7
  24. package/dist/dashboard/types.d.ts +1 -0
  25. package/dist/engines/client-bootstrap.js +18 -24
  26. package/dist/engines/mcp-entrypoint.d.ts +12 -0
  27. package/dist/engines/mcp-entrypoint.js +33 -0
  28. package/dist/engines/orchestrator.js +36 -8
  29. package/dist/engines/pattern-registry.d.ts +12 -0
  30. package/dist/engines/pattern-registry.js +65 -0
  31. package/dist/engines/specialist-roster.js +16 -8
  32. package/dist/engines/user-workflow-trace.d.ts +47 -0
  33. package/dist/engines/user-workflow-trace.js +281 -0
  34. package/dist/phantom/runtime.d.ts +4 -0
  35. package/dist/phantom/runtime.js +31 -1
  36. 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));
@@ -15,6 +15,7 @@ export const FIRST_CLASS_TOOLS = new Set([
15
15
  'nexus_search',
16
16
  'nexus_runtime_health',
17
17
  'nexus_run_status',
18
+ 'nexus_user_workflow_trace',
18
19
  'kernel_run_step',
19
20
  'kernel_run_verify',
20
21
  'kernel_run_merge',
@@ -881,6 +882,18 @@ export function buildMcpToolDefinitions() {
881
882
  required: ['runId'],
882
883
  },
883
884
  },
885
+ {
886
+ name: 'nexus_user_workflow_trace',
887
+ description: 'Return the local user-agent workflow trace for a run: user goal, agent actions, context, verification, failure signals, and next actions.',
888
+ inputSchema: {
889
+ type: 'object',
890
+ properties: {
891
+ runId: { type: 'string', description: 'Execution run ID' },
892
+ format: { type: 'string', enum: ['text', 'json'], description: 'Response format' },
893
+ },
894
+ required: ['runId'],
895
+ },
896
+ },
884
897
  // ── Darwin Loop ──────────────────────────────────────────────────
885
898
  {
886
899
  name: 'nexus_darwin_propose',
@@ -10,5 +10,7 @@ type McpResult = {
10
10
  text: string;
11
11
  }>;
12
12
  };
13
+ export declare function extractSkillSelectorsFromPrompt(prompt: string): string[];
14
+ export declare function inferSpawnWorkersIntent(actions: unknown[]): 'plan' | 'mutate';
13
15
  export declare function handleOrchestrationGroup(toolName: string, hctx: McpHandlerCtx, request: any, args: Record<string, unknown>, ctx?: any): Promise<McpResult | undefined>;
14
16
  export {};
@@ -20,6 +20,25 @@ import { getSharedTelemetry } from '../../../../engines/telemetry-remote.js';
20
20
  import { requireRuntime } from '../util/require-runtime.js';
21
21
  import { ensureCrGraphBuilt } from '../../../../engines/code-review-graph-client.js';
22
22
  import { recordFirstBootstrap } from '../../../../engines/telemetry.js';
23
+ export function extractSkillSelectorsFromPrompt(prompt) {
24
+ const selectors = new Set();
25
+ const add = (value) => {
26
+ const cleaned = value?.trim().replace(/^[$@]+/, '');
27
+ if (cleaned && /^[a-z0-9][a-z0-9._-]{1,120}$/i.test(cleaned)) {
28
+ selectors.add(cleaned);
29
+ }
30
+ };
31
+ for (const match of prompt.matchAll(/\[[$@]?([a-z0-9][a-z0-9._-]{1,120})\]\([^)]*\/\.agents?\/skills\/[^)]*\/SKILL\.md\)/gi)) {
32
+ add(match[1]);
33
+ }
34
+ for (const match of prompt.matchAll(/\/\.agents?\/skills\/([^/\s)]+)\/SKILL\.md/gi)) {
35
+ add(match[1]);
36
+ }
37
+ return [...selectors];
38
+ }
39
+ export function inferSpawnWorkersIntent(actions) {
40
+ return actions.length > 0 ? 'mutate' : 'plan';
41
+ }
23
42
  export async function handleOrchestrationGroup(toolName, hctx, request, args, ctx) {
24
43
  const runtimeError = requireRuntime(hctx);
25
44
  if (runtimeError)
@@ -315,9 +334,11 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
315
334
  const workers = request.params.arguments?.workers == null
316
335
  ? undefined
317
336
  : Number(request.params.arguments.workers);
318
- const skills = Array.isArray(request.params.arguments?.skills)
337
+ const explicitSkills = Array.isArray(request.params.arguments?.skills)
319
338
  ? request.params.arguments.skills.map(String)
320
- : undefined;
339
+ : [];
340
+ const promptSkills = extractSkillSelectorsFromPrompt(prompt);
341
+ const skills = [...new Set([...explicitSkills, ...promptSkills])];
321
342
  const workflows = Array.isArray(request.params.arguments?.workflows)
322
343
  ? request.params.arguments.workflows.map(String)
323
344
  : undefined;
@@ -374,7 +395,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
374
395
  const execution = await hctx.nexusRef.orchestrate(prompt, applyExecutionPreset({
375
396
  files,
376
397
  workers,
377
- skillNames: skills,
398
+ skillNames: skills.length > 0 ? skills : undefined,
378
399
  workflowSelectors: workflows,
379
400
  hookSelectors: hooks,
380
401
  automationSelectors: automations,
@@ -769,6 +790,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
769
790
  ? String(request.params.arguments.executionPreset)
770
791
  : undefined;
771
792
  const preset = resolveExecutionPreset(executionPreset);
793
+ const inferredIntent = inferSpawnWorkersIntent(actions);
772
794
  // Surface worker-plan intent on the SSE feed without persisting it as a memory.
773
795
  try {
774
796
  nexusEventBus.emit('memory.snapshot', {
@@ -796,11 +818,18 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
796
818
  backendSelectors: { memoryBackend, compressionBackend, dslCompiler },
797
819
  backendMode,
798
820
  optimizationProfile,
821
+ intent: inferredIntent,
799
822
  executionMode: 'manual-low-level',
800
823
  manualOverrides: ['nexus_spawn_workers'],
801
824
  }, executionPreset));
802
825
  const verifiedWorkers = execution.workerResults.filter(result => result.verified).length;
803
826
  const modifiedFiles = execution.workerResults.reduce((sum, result) => sum + result.modifiedFiles.length, 0);
827
+ const noDiffInspected = execution.state === 'inspected'
828
+ && modifiedFiles === 0
829
+ && !execution.workerResults.some((result) => result.diff?.trim());
830
+ const displayedDecision = noDiffInspected
831
+ ? 'no-applicable-diff'
832
+ : execution.finalDecision?.action ?? 'none';
804
833
  execution.activeSkills.forEach(skill => {
805
834
  hctx.sessionDNA.recordSkill(skill.name);
806
835
  if (skill.scope === 'global' || skill.rolloutStatus === 'promoted') {
@@ -826,7 +855,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
826
855
  `Run: ${execution.runId.padEnd(28, ' ')} State: ${execution.state.padEnd(18, ' ')}`,
827
856
  `Workers: ${execution.workerResults.length.toString().padEnd(5, ' ')} Verified: ${verifiedWorkers.toString().padEnd(10, ' ')} Files: ${String(modifiedFiles).padEnd(12, ' ')}`,
828
857
  `${`Preset: ${preset?.name ?? 'manual'}`.padEnd(61, ' ')}`,
829
- `Decision: ${(execution.finalDecision?.action ?? 'none').padEnd(52, ' ')}`
858
+ `Decision: ${displayedDecision.padEnd(52, ' ')}`
830
859
  ], execution.state === 'merged' || execution.state === 'inspected' ? '32' : execution.state === 'rolled_back' ? '33' : '31');
831
860
  return {
832
861
  content: [{
@@ -841,7 +870,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
841
870
  preset ? `Preset: ${preset.name} (${preset.id})` : null,
842
871
  `Verified Workers: ${verifiedWorkers}`,
843
872
  `Modified Files: ${modifiedFiles}`,
844
- `Decision: ${execution.finalDecision?.action ?? 'none'}`,
873
+ `Decision: ${displayedDecision}`,
845
874
  `Recommended Strategy: ${execution.finalDecision?.recommendedStrategy ?? 'n/a'}`,
846
875
  `Planner: ${execution.plannerResult?.summary ?? 'n/a'}`,
847
876
  `Crew: ${execution.plannerResult?.selectedCrew?.name ?? 'n/a'}`,
@@ -4,7 +4,7 @@
4
4
  * federation_status, run_status.
5
5
  * Extracted from mcp.ts (Phase 3 split).
6
6
  */
7
- import { formatBullets, formatJsonDetails, buildAutoMemorySummary, } from '../helpers.js';
7
+ import { formatBullets, formatJsonDetails, truncateText, buildAutoMemorySummary, } from '../helpers.js';
8
8
  import { nexusEventBus } from '../../../../engines/event-bus.js';
9
9
  import { getSharedLicenseManager, snapshotPCU, formatPCUStatus } from '../../../../licensing/index.js';
10
10
  import { TokenAnalyticsEngine } from '../../../../engines/token-analytics.js';
@@ -13,6 +13,7 @@ import { requireRuntime } from '../util/require-runtime.js';
13
13
  import { getAsyncGate } from '../async-gate.js';
14
14
  import { getRun as getOrchRun } from '../../../../engines/orchestrator/store.js';
15
15
  import { nxlResult } from '../../../../nxl/index.js';
16
+ import { summarizeUserWorkflowTrace } from '../../../../engines/user-workflow-trace.js';
16
17
  export async function handleRuntimeGroup(toolName, hctx, request, args, ctx) {
17
18
  const runtimeError = requireRuntime(hctx);
18
19
  if (runtimeError)
@@ -226,6 +227,40 @@ export async function handleRuntimeGroup(toolName, hctx, request, args, ctx) {
226
227
  }],
227
228
  };
228
229
  }
230
+ case 'nexus_user_workflow_trace': {
231
+ const runId = String(request.params.arguments?.runId ?? '');
232
+ const fmt = String(request.params.arguments?.format ?? 'text');
233
+ const trace = await hctx.getRuntime().getUserWorkflowTrace?.(runId);
234
+ if (!trace) {
235
+ return { content: [{ type: 'text', text: `User workflow trace not found: ${runId}` }] };
236
+ }
237
+ if (fmt === 'json') {
238
+ return { content: [{ type: 'text', text: JSON.stringify(trace, null, 2) }] };
239
+ }
240
+ return {
241
+ content: [{
242
+ type: 'text',
243
+ text: [
244
+ summarizeUserWorkflowTrace(trace),
245
+ '',
246
+ formatBullets([
247
+ `Run: ${trace.runId}`,
248
+ `Goal: ${truncateText(trace.goal, 120)}`,
249
+ `Actions: ${trace.userJourney.actions.length ? trace.userJourney.actions.slice(0, 5).join(' | ') : 'none recorded'}`,
250
+ `Files: ${trace.userJourney.selectedFiles.length ? trace.userJourney.selectedFiles.slice(0, 5).join(', ') : 'none selected'}`,
251
+ `Agents: ${trace.userJourney.selectedSpecialists.length ? trace.userJourney.selectedSpecialists.slice(0, 5).join(', ') : 'none selected'}`,
252
+ `Signals: ${trace.signals.length ? trace.signals.map(signal => `${signal.severity}:${signal.code}`).slice(0, 8).join(', ') : 'none'}`,
253
+ ]),
254
+ trace.signals.length
255
+ ? `Failure signals\n${formatBullets(trace.signals.map(signal => `${signal.code}: ${signal.summary}`))}`
256
+ : '',
257
+ trace.nextActions.length
258
+ ? `Next actions\n${formatBullets(trace.nextActions)}`
259
+ : '',
260
+ ].filter(Boolean).join('\n\n'),
261
+ }],
262
+ };
263
+ }
229
264
  case 'nexus_run_status': {
230
265
  const runId = String(request.params.arguments?.runId ?? '');
231
266
  const fmt = String(request.params.arguments?.format ?? 'text');
@@ -190,7 +190,7 @@ export function isReadOnlyMcpTool(toolName) {
190
190
  'nexus_list_', 'nexus_describe_', 'nexus_runtime_health',
191
191
  'nexus_doctor', 'nexus_token_report', 'nexus_recall_memory',
192
192
  'nexus_temporal_query', 'nexus_memory_stats', 'nexus_federation_status',
193
- 'nexus_license_usage', 'nexus_run_status', 'nexus_selection_ledger',
193
+ 'nexus_license_usage', 'nexus_run_status', 'nexus_user_workflow_trace', 'nexus_selection_ledger',
194
194
  'nexus_operative_journal',
195
195
  ];
196
196
  return skipPrefixes.some(p => toolName.startsWith(p));
@@ -11,6 +11,7 @@ import { dirname, join, resolve } from 'path';
11
11
  import { detectInstalledIDEs, getMcpConfigForIDE } from '../agents/adapters/ide-compat.js';
12
12
  import { DEFAULT_DAEMON_READY_TIMEOUT_MS, ensureDaemonReady } from '../daemon/client.js';
13
13
  import { getSetupDefinition, installSetup } from '../engines/client-bootstrap.js';
14
+ import { buildNexusMcpServerConfig } from '../engines/mcp-entrypoint.js';
14
15
  import { resolveNexusStateDir } from '../engines/runtime-registry.js';
15
16
  import { resolveWorkspaceContext } from '../engines/workspace-resolver.js';
16
17
  import { runPostinstallCleanup } from '../postinstall/cleanup.js';
@@ -136,14 +137,7 @@ export async function configureIDE(ide, opts = {}) {
136
137
  }
137
138
  /** Nexus Prime MCP server entry used in workspace-local config writes. */
138
139
  function _nexusMcpEntry(workspaceRoot) {
139
- return {
140
- command: 'node',
141
- args: [join(workspaceRoot, 'node_modules', 'nexus-prime', 'dist', 'cli.js'), 'mcp'],
142
- env: {
143
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
144
- NEXUS_WORKSPACE_ROOT: workspaceRoot,
145
- },
146
- };
140
+ return buildNexusMcpServerConfig(workspaceRoot);
147
141
  }
148
142
  /**
149
143
  * Write Claude Code PreToolUse/PostToolUse/UserPromptSubmit/Stop hooks to
@@ -379,18 +373,15 @@ function mergeIntoExistingConfig(configPath, newContent, ide) {
379
373
  }
380
374
  /** Print how to add nexus-prime manually to an MCP client. */
381
375
  export function printManualMcpInstructions() {
376
+ const server = buildNexusMcpServerConfig(process.cwd());
382
377
  const snippet = JSON.stringify({
383
378
  mcpServers: {
384
- 'nexus-prime': {
385
- command: 'node',
386
- args: ['node_modules/nexus-prime/dist/cli.js', 'mcp'],
387
- env: {},
388
- },
379
+ 'nexus-prime': server,
389
380
  },
390
381
  }, null, 2);
391
382
  process.stdout.write('\n Add this to your IDE\'s MCP configuration:\n\n');
392
383
  process.stdout.write(snippet.split('\n').map(l => ` ${l}`).join('\n') + '\n\n');
393
- process.stdout.write(' Or run: npx nexus-prime@latest setup\n\n');
384
+ process.stdout.write(' Or run: nexus-prime setup\n\n');
394
385
  }
395
386
  /** Entry point for CLI: nexus-prime install / setup auto */
396
387
  export async function cliSetup(opts = []) {
package/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ import { InstructionGateway } from './engines/instruction-gateway.js';
18
18
  import { ensureBootstrap, collectBootstrapManifest, validateTargetPath, resolveCodexConfigPath, renderCodexMcpTomlConfig, writeCodexMcpConfig, hasExpectedCodexConfig, } from './engines/client-bootstrap.js';
19
19
  import { resolveNexusStateDir } from './engines/runtime-registry.js';
20
20
  import { runStartupHygiene } from './engines/runtime-hygiene.js';
21
+ import { buildNexusMcpServerConfig, isStableNexusMcpServerConfig } from './engines/mcp-entrypoint.js';
21
22
  import { SessionDNAManager } from './engines/session-dna.js';
22
23
  import { nexusEventBus } from './engines/event-bus.js';
23
24
  import { buildRuntimeSetupCommand } from './cli-setup.js';
@@ -189,14 +190,7 @@ function readJson(targetPath) {
189
190
  }
190
191
  }
191
192
  function buildStandardMcpServerConfig(workspaceRoot) {
192
- return {
193
- command: 'npx',
194
- args: ['-y', 'nexus-prime', 'mcp'],
195
- env: {
196
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
197
- ...(workspaceRoot ? { NEXUS_WORKSPACE_ROOT: workspaceRoot } : {}),
198
- }
199
- };
193
+ return buildNexusMcpServerConfig(workspaceRoot);
200
194
  }
201
195
  function writeStandardMcpConfig(targetPath, workspaceRoot) {
202
196
  const existing = readJson(targetPath);
@@ -207,13 +201,13 @@ function writeStandardMcpConfig(targetPath, workspaceRoot) {
207
201
  }
208
202
  function writeOpencodeConfig(targetPath, workspaceRoot) {
209
203
  const existing = readJson(targetPath);
204
+ const mcpConfig = buildStandardMcpServerConfig(workspaceRoot);
210
205
  const server = {
211
206
  type: 'local',
212
- command: 'npx',
213
- args: ['-y', 'nexus-prime', 'mcp'],
207
+ command: mcpConfig.command,
208
+ args: mcpConfig.args,
214
209
  environment: {
215
- NEXUS_MCP_TOOL_PROFILE: 'autonomous',
216
- ...(workspaceRoot ? { NEXUS_WORKSPACE_ROOT: workspaceRoot } : {}),
210
+ ...mcpConfig.env,
217
211
  }
218
212
  };
219
213
  existing.mcp = existing.mcp ?? {};
@@ -251,21 +245,19 @@ function mergeCodexAgentsContent(existingContent, content) {
251
245
  const endIndex = existing.indexOf(CODEX_MANAGED_END);
252
246
  if (startIndex >= 0 && endIndex > startIndex) {
253
247
  const before = existing.slice(0, startIndex).trimEnd();
254
- const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trimStart();
248
+ const after = existing.slice(endIndex + CODEX_MANAGED_END.length).trim();
255
249
  return [
256
250
  before,
257
251
  before ? '' : undefined,
258
252
  managedBlock,
259
253
  after ? '' : undefined,
260
254
  after,
261
- '',
262
255
  ].filter((value) => value !== undefined).join('\n');
263
256
  }
264
257
  return [
265
258
  existing.trimEnd(),
266
259
  '',
267
260
  managedBlock,
268
- '',
269
261
  ].join('\n');
270
262
  }
271
263
  function hasCurrentCodexManagedBlock(targetPath, content) {
@@ -301,17 +293,16 @@ function mergeClaudeContent(existingContent, content) {
301
293
  const endIndex = existing.indexOf(CLAUDE_MANAGED_END);
302
294
  if (startIndex >= 0 && endIndex > startIndex) {
303
295
  const before = existing.slice(0, startIndex).trimEnd();
304
- const after = existing.slice(endIndex + CLAUDE_MANAGED_END.length).trimStart();
296
+ const after = existing.slice(endIndex + CLAUDE_MANAGED_END.length).trim();
305
297
  return [
306
298
  before,
307
299
  before ? '' : undefined,
308
300
  managedBlock,
309
301
  after ? '' : undefined,
310
302
  after,
311
- '',
312
303
  ].filter((value) => value !== undefined).join('\n');
313
304
  }
314
- return [existing.trimEnd(), '', managedBlock, ''].join('\n');
305
+ return [existing.trimEnd(), '', managedBlock].join('\n');
315
306
  }
316
307
  function hasCurrentClaudeManagedBlock(targetPath, content) {
317
308
  if (!existsSync(targetPath))
@@ -346,17 +337,16 @@ function mergeClinerules(existingContent, content) {
346
337
  const endIndex = existing.indexOf(CLINE_MANAGED_END);
347
338
  if (startIndex >= 0 && endIndex > startIndex) {
348
339
  const before = existing.slice(0, startIndex).trimEnd();
349
- const after = existing.slice(endIndex + CLINE_MANAGED_END.length).trimStart();
340
+ const after = existing.slice(endIndex + CLINE_MANAGED_END.length).trim();
350
341
  return [
351
342
  before,
352
343
  before ? '' : undefined,
353
344
  managedBlock,
354
345
  after ? '' : undefined,
355
346
  after,
356
- '',
357
347
  ].filter((value) => value !== undefined).join('\n');
358
348
  }
359
- return [existing.trimEnd(), '', managedBlock, ''].join('\n');
349
+ return [existing.trimEnd(), '', managedBlock].join('\n');
360
350
  }
361
351
  function hasCurrentClineManagedBlock(targetPath, content) {
362
352
  if (!existsSync(targetPath))
@@ -591,12 +581,10 @@ function hasExpectedConfig(definition) {
591
581
  const parsed = JSON.parse(readFileSync(definition.configPath, 'utf8'));
592
582
  if (definition.id === 'opencode') {
593
583
  const server = parsed?.mcp?.['nexus-prime'];
594
- return Boolean(server
595
- && server?.environment?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
584
+ return Boolean(server && isStableNexusMcpServerConfig(server, 'environment'));
596
585
  }
597
586
  const server = parsed?.mcpServers?.['nexus-prime'];
598
- return Boolean(server
599
- && server?.env?.NEXUS_MCP_TOOL_PROFILE === 'autonomous');
587
+ return Boolean(server && isStableNexusMcpServerConfig(server));
600
588
  }
601
589
  catch {
602
590
  return false;
@@ -825,7 +813,8 @@ program
825
813
  const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
826
814
  const caller = detectCallerIDE();
827
815
  const marker = readSetupMarker();
828
- if (caller) {
816
+ const autoConfigEnabled = process.env.NEXUS_MCP_AUTO_CONFIG === '1';
817
+ if (caller && autoConfigEnabled) {
829
818
  const isConfigured = marker?.configuredIDEs?.includes(caller) ?? false;
830
819
  const storedHash = marker?.writtenHash?.[caller];
831
820
  if (isConfigured && storedHash !== undefined) {
@@ -879,6 +868,10 @@ program
879
868
  }
880
869
  }
881
870
  // Try daemon-backed proxy first unless --standalone is passed
871
+ const parsedDaemonTimeout = Number(process.env.NEXUS_MCP_DAEMON_TIMEOUT_MS ?? 5000);
872
+ const mcpDaemonTimeoutMs = Number.isFinite(parsedDaemonTimeout)
873
+ ? Math.max(1000, Math.min(parsedDaemonTimeout, 15000))
874
+ : 5000;
882
875
  if (!options.standalone) {
883
876
  try {
884
877
  const status = await getDaemonStatus(workspaceContext);
@@ -886,6 +879,7 @@ program
886
879
  console.error('Connecting to Nexus Prime daemon...');
887
880
  await startDaemonBackedMcpProxy(workspaceContext, {
888
881
  entrypoint: getCliEntrypoint(),
882
+ timeoutMs: mcpDaemonTimeoutMs,
889
883
  });
890
884
  return;
891
885
  }
@@ -900,9 +894,11 @@ program
900
894
  try {
901
895
  await ensureDaemonReady(workspaceContext, {
902
896
  entrypoint: getCliEntrypoint(),
897
+ timeoutMs: mcpDaemonTimeoutMs,
903
898
  });
904
899
  await startDaemonBackedMcpProxy(workspaceContext, {
905
900
  entrypoint: getCliEntrypoint(),
901
+ timeoutMs: mcpDaemonTimeoutMs,
906
902
  });
907
903
  return;
908
904
  }
@@ -1,4 +1,5 @@
1
1
  import type { WorkspaceContext } from '../engines/workspace-resolver.js';
2
2
  export declare function startDaemonBackedMcpProxy(workspace: WorkspaceContext, options?: {
3
3
  entrypoint?: string;
4
+ timeoutMs?: number;
4
5
  }): Promise<void>;
@@ -3,12 +3,15 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { daemonRequest, ensureDaemonReady, } from './client.js';
6
- async function withDaemonRetry(workspace, daemonRef, entrypoint, operation) {
6
+ async function withDaemonRetry(workspace, daemonRef, options, operation) {
7
7
  try {
8
8
  return await operation(daemonRef.current);
9
9
  }
10
10
  catch {
11
- daemonRef.current = await ensureDaemonReady(workspace, { entrypoint });
11
+ daemonRef.current = await ensureDaemonReady(workspace, {
12
+ entrypoint: options.entrypoint,
13
+ timeoutMs: options.timeoutMs,
14
+ });
12
15
  return operation(daemonRef.current);
13
16
  }
14
17
  }
@@ -16,18 +19,19 @@ export async function startDaemonBackedMcpProxy(workspace, options = {}) {
16
19
  const daemonRef = {
17
20
  current: await ensureDaemonReady(workspace, {
18
21
  entrypoint: options.entrypoint,
22
+ timeoutMs: options.timeoutMs,
19
23
  }),
20
24
  };
21
25
  const sessionId = randomUUID();
22
26
  const server = new Server({ name: 'nexus-prime-mcp-proxy', version: '0.1.0' }, { capabilities: { tools: {} } });
23
27
  server.setRequestHandler(ListToolsRequestSchema, async () => {
24
- const payload = await withDaemonRetry(workspace, daemonRef, options.entrypoint, (record) => daemonRequest(record, '/rpc/list-tools', { sessionId }));
28
+ const payload = await withDaemonRetry(workspace, daemonRef, options, (record) => daemonRequest(record, '/rpc/list-tools', { sessionId }));
25
29
  return {
26
30
  tools: payload.tools,
27
31
  };
28
32
  });
29
33
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
30
- const payload = await withDaemonRetry(workspace, daemonRef, options.entrypoint, (record) => daemonRequest(record, '/rpc/call-tool', {
34
+ const payload = await withDaemonRetry(workspace, daemonRef, options, (record) => daemonRequest(record, '/rpc/call-tool', {
31
35
  sessionId,
32
36
  name: request.params?.name,
33
37
  arguments: request.params?.arguments ?? {},
@@ -9,21 +9,35 @@ import { CACHE_TTL, S, bus } from './state.js';
9
9
 
10
10
  const _cache = new Map();
11
11
 
12
+ function _scopedUrl(url) {
13
+ if (typeof url !== 'string' || !url.startsWith('/api/')) return url;
14
+ const runtimeId = S.scope?.runtimeId;
15
+ if (!runtimeId) return url;
16
+ try {
17
+ const scoped = new URL(url, window.location.origin);
18
+ if (!scoped.searchParams.has('runtimeId')) scoped.searchParams.set('runtimeId', runtimeId);
19
+ return scoped.pathname + scoped.search + scoped.hash;
20
+ } catch {
21
+ return url;
22
+ }
23
+ }
24
+
12
25
  /**
13
26
  * GET with SWR semantics: return cached data immediately (if within TTL),
14
27
  * then kick off a background refresh. Callers get the fast synchronous hit
15
28
  * for p50 <5ms; the next render cycle gets the fresh value.
16
29
  */
17
30
  export async function api(url, ttl = CACHE_TTL) {
18
- const hit = _cache.get(url);
31
+ const requestUrl = _scopedUrl(url);
32
+ const hit = _cache.get(requestUrl);
19
33
  const fresh = !hit || (Date.now() - hit.ts >= ttl);
20
34
 
21
35
  // Return stale data synchronously while refresh fires in background.
22
- const refreshPromise = fresh ? _fetch(url) : Promise.resolve(hit.data);
36
+ const refreshPromise = fresh ? _fetch(requestUrl) : Promise.resolve(hit.data);
23
37
 
24
38
  if (hit && !fresh) {
25
39
  // Background refresh — don't await, just schedule.
26
- _fetch(url).catch(() => {});
40
+ _fetch(requestUrl).catch(() => {});
27
41
  }
28
42
 
29
43
  return refreshPromise;
@@ -52,9 +66,10 @@ async function _fetch(url) {
52
66
  */
53
67
  export async function post(url, body, opts = {}) {
54
68
  const { optimistic } = opts;
69
+ const requestUrl = _scopedUrl(url);
55
70
  // If an optimistic record is provided, return it right away and let the
56
71
  // caller reconcile once the real response arrives.
57
- const req = fetch(url, {
72
+ const req = fetch(requestUrl, {
58
73
  method: 'POST',
59
74
  headers: { 'Content-Type': 'application/json' },
60
75
  body: JSON.stringify(body),
@@ -77,7 +92,7 @@ export async function post(url, body, opts = {}) {
77
92
  if (optimistic !== undefined) {
78
93
  // Return immediately with optimistic data; the caller can await the
79
94
  // real result separately if it needs to reconcile.
80
- req.then((real) => { if (!real.ok) bus.emit('post:optimistic-fail', { url, error: real.error }); });
95
+ req.then((real) => { if (!real.ok) bus.emit('post:optimistic-fail', { url: requestUrl, error: real.error }); });
81
96
  return { ok: true, data: optimistic, error: null, status: 202, realResponse: req };
82
97
  }
83
98
  return req;
@@ -86,6 +101,8 @@ export async function post(url, body, opts = {}) {
86
101
  /** Invalidate a cached URL so the next api() call fetches fresh. */
87
102
  export function bustCache(url) {
88
103
  _cache.delete(url);
104
+ const scoped = _scopedUrl(url);
105
+ if (scoped !== url) _cache.delete(scoped);
89
106
  }
90
107
 
91
108
  /** Clear the entire cache (e.g. after an SSE reconnect). */
@@ -122,6 +139,15 @@ export function notifyNotReady(payloads) {
122
139
  for (const p of payloads) {
123
140
  if (!p || typeof p !== 'object' || !p.notReady) continue;
124
141
  const code = String(p.pillar || 'pillar') + '-not-ready';
125
- pushSystemError(code, p.reason || 'runtime not attached', undefined);
142
+ const reason = String(p.reason || '');
143
+ const optionalIdleRuntime = /not attached|not initiali[sz]ed|deferred|lazy/i.test(reason)
144
+ && !p.error
145
+ && !p.fatal
146
+ && !p.severity;
147
+ if (optionalIdleRuntime) {
148
+ clearSystemError(code);
149
+ continue;
150
+ }
151
+ pushSystemError(code, reason || 'runtime not attached', undefined);
126
152
  }
127
153
  }
@@ -185,6 +185,10 @@ if(location.protocol==='file:'){
185
185
  <div class="shd">Promotion history</div>
186
186
  <div class="card" style="margin-bottom:14px"><div id="mem-promotion-ticker" class="mem-promotion-ticker"></div></div>
187
187
 
188
+ <div class="memory-toolbar">
189
+ <button class="btn btn-sm" id="mem-graph-max-btn">Maximize graph</button>
190
+ <button class="btn btn-sm" id="mem-browse-btn">Browse memories</button>
191
+ </div>
188
192
  <div id="graph-container" class="card">
189
193
  <svg id="graph-svg"></svg>
190
194
  <div class="graph-legend">
@@ -208,6 +212,7 @@ if(location.protocol==='file:'){
208
212
  <div class="card">
209
213
  <div class="mem-search-row">
210
214
  <input id="mem-search" type="text" placeholder="Search memories…" aria-label="Search memories">
215
+ <button class="btn btn-sm" id="mem-list-browse-btn">Browse</button>
211
216
  </div>
212
217
  <div id="mem-list"></div>
213
218
  </div>