nexus-prime 6.5.0 → 6.6.0

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.
@@ -364,17 +364,40 @@ export async function cliSetup(opts = []) {
364
364
  if (cleanup.staleLockfilesRemoved + cleanup.staleDaemonsKilled > 0) {
365
365
  log(` ${c('↺', 'dim')} Cleaned ${cleanup.staleLockfilesRemoved} lockfiles, stopped ${cleanup.staleDaemonsKilled} stale daemons`);
366
366
  }
367
- // Step 1 — detect + configure IDEs
367
+ // Step 1 — detect + configure IDEs (per-IDE animated checklist)
368
368
  const totalSteps = options.licenseKey ? 5 : 4;
369
369
  step(1, totalSteps, 'Detecting IDEs…');
370
+ // Emit install.step event helper (best-effort; daemon may not be running yet)
371
+ const emitInstallStep = (stepN, total, label, status, detail) => {
372
+ try {
373
+ import('../engines/event-bus.js').then(({ nexusEventBus }) => {
374
+ nexusEventBus.emit('install.step', { step: stepN, total, label, status, detail });
375
+ }).catch(() => { });
376
+ }
377
+ catch { /* non-fatal */ }
378
+ };
370
379
  const result = await runInstallWizard({ dryRun, verbose: false });
371
- if (result.configured.length > 0) {
372
- log(` ${c('✓', 'green')} Configured: ${c(result.configured.join(', '), 'cyan')}`);
373
- }
374
- if (result.skipped.length > 0) {
375
- log(` ${c('✓', 'green')} Already set up: ${c(result.skipped.join(', '), 'dim')}`);
380
+ const allIDEs = [...result.configured, ...result.skipped, ...result.errors.map(e => e.ide)];
381
+ if (allIDEs.length > 0) {
382
+ for (const ide of allIDEs) {
383
+ const isConfigured = result.configured.includes(ide);
384
+ const isSkipped = result.skipped.includes(ide);
385
+ const errEntry = result.errors.find(e => e.ide === ide);
386
+ if (isConfigured) {
387
+ await withSpinner(`${ide} · configuring`, Promise.resolve());
388
+ emitInstallStep(1, totalSteps, ide, 'ok', 'MCP config written');
389
+ }
390
+ else if (isSkipped) {
391
+ log(` ${c('─', 'dim')} ${c(ide, 'dim')} already set up`);
392
+ emitInstallStep(1, totalSteps, ide, 'skip', 'already configured');
393
+ }
394
+ else if (errEntry) {
395
+ log(` ${c('✗', 'yellow')} ${c(ide, 'yellow')} · ${errEntry.reason}`);
396
+ emitInstallStep(1, totalSteps, ide, 'fail', errEntry.reason);
397
+ }
398
+ }
376
399
  }
377
- if (result.configured.length === 0 && result.skipped.length === 0) {
400
+ else {
378
401
  log(` ${c('—', 'dim')} No IDEs auto-detected. Run: nexus-prime setup <ide>`);
379
402
  }
380
403
  // Step 2 — MCP configs confirmed
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Nexus Prime — `nexus-prime watch`
3
+ * Tail live SSE events from the running daemon in a colored terminal feed.
4
+ */
5
+ export interface WatchOptions {
6
+ port?: number;
7
+ all?: boolean;
8
+ filter?: string;
9
+ }
10
+ export declare function runWatch(opts?: WatchOptions): Promise<void>;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Nexus Prime — `nexus-prime watch`
3
+ * Tail live SSE events from the running daemon in a colored terminal feed.
4
+ */
5
+ import * as http from 'http';
6
+ const ANSI = {
7
+ cyan: '\x1b[36m',
8
+ green: '\x1b[32m',
9
+ yellow: '\x1b[33m',
10
+ magenta: '\x1b[35m',
11
+ red: '\x1b[31m',
12
+ dim: '\x1b[2m',
13
+ bold: '\x1b[1m',
14
+ reset: '\x1b[0m',
15
+ };
16
+ const isTTY = process.stdout.isTTY === true && !process.env.NO_COLOR;
17
+ const c = (s, k) => isTTY ? `${ANSI[k]}${s}${ANSI.reset}` : s;
18
+ const DEFAULT_FILTER = /^(mcp\.|tokens\.|planner\.|phantom\.|license\.|install\.|orchestrator\.run)/;
19
+ function colorForType(type) {
20
+ if (type.startsWith('mcp.'))
21
+ return 'cyan';
22
+ if (type.startsWith('tokens.'))
23
+ return 'yellow';
24
+ if (type.startsWith('license.'))
25
+ return 'magenta';
26
+ if (type.startsWith('phantom.'))
27
+ return 'green';
28
+ if (type.startsWith('install.'))
29
+ return 'green';
30
+ if (type.includes('error') || type.includes('fail') || type.includes('bad'))
31
+ return 'red';
32
+ return 'dim';
33
+ }
34
+ export async function runWatch(opts = {}) {
35
+ const port = opts.port ?? Number(process.env.NEXUS_DASHBOARD_PORT ?? 3377);
36
+ const filterRe = opts.all
37
+ ? null
38
+ : opts.filter
39
+ ? new RegExp(opts.filter)
40
+ : DEFAULT_FILTER;
41
+ console.log(c('◆ Nexus Prime · Live Event Tail', 'cyan'));
42
+ console.log(c(` Stream: http://localhost:${port}/dashboard/events`, 'dim'));
43
+ console.log(c(` Filter: ${filterRe ? filterRe.source : 'all events'}`, 'dim'));
44
+ console.log(c(' Press Ctrl-C to exit\n', 'dim'));
45
+ let buf = '';
46
+ const req = http.get({ hostname: 'localhost', port, path: '/dashboard/events', headers: { Accept: 'text/event-stream' } }, (res) => {
47
+ if (res.statusCode !== 200) {
48
+ console.error(c(` ✗ Daemon not reachable (HTTP ${res.statusCode}). Start: nexus-prime daemon`, 'red'));
49
+ process.exit(1);
50
+ }
51
+ res.setEncoding('utf8');
52
+ res.on('data', (chunk) => {
53
+ buf += chunk;
54
+ const lines = buf.split('\n');
55
+ buf = lines.pop() ?? '';
56
+ for (const line of lines) {
57
+ if (!line.startsWith('data: '))
58
+ continue;
59
+ try {
60
+ const evt = JSON.parse(line.slice(6));
61
+ const type = evt.type ?? evt.t ?? '';
62
+ if (filterRe && !filterRe.test(type))
63
+ continue;
64
+ const payload = evt.payload ?? evt.data ?? evt.p ?? {};
65
+ const summary = summarize(type, payload);
66
+ const ts = new Date().toTimeString().slice(0, 8);
67
+ process.stdout.write(`${c(ts, 'dim')} ${c(type.padEnd(36), colorForType(type))} ${summary}\n`);
68
+ }
69
+ catch { /* malformed JSON — skip */ }
70
+ }
71
+ });
72
+ res.on('error', (err) => {
73
+ console.error(c(` Stream error: ${err.message}`, 'red'));
74
+ process.exit(1);
75
+ });
76
+ res.on('close', () => {
77
+ console.log(c('\n Stream closed.', 'dim'));
78
+ process.exit(0);
79
+ });
80
+ });
81
+ req.on('error', (err) => {
82
+ console.error(c(` ✗ Cannot connect to daemon on port ${port}: ${err.message}`, 'red'));
83
+ console.error(c(' Start daemon: nexus-prime daemon', 'dim'));
84
+ process.exit(1);
85
+ });
86
+ process.on('SIGINT', () => {
87
+ req.destroy();
88
+ console.log(c('\n Bye.', 'dim'));
89
+ process.exit(0);
90
+ });
91
+ }
92
+ function summarize(type, payload) {
93
+ switch (type) {
94
+ case 'mcp.call.start':
95
+ return `→ ${payload.toolName ?? 'tool'}`;
96
+ case 'mcp.call.complete':
97
+ return payload.error
98
+ ? c(`✗ ${payload.toolName ?? 'tool'} · ${String(payload.error).slice(0, 60)}`, 'red')
99
+ : c(`✓ ${payload.toolName ?? 'tool'} · ${payload.durationMs ?? 0}ms`, 'green');
100
+ case 'tokens.optimized':
101
+ return `saved ${payload.savings ?? 0} · ${payload.pct ?? 0}% · src:${payload.source ?? 'unknown'}`;
102
+ case 'planner.stage':
103
+ return `${payload.stage ?? ''} · ${payload.status ?? ''}`;
104
+ case 'orchestrator.run.start':
105
+ return String(payload.goal ?? '').slice(0, 80);
106
+ case 'orchestrator.run.complete':
107
+ return `done · ${payload.durationMs ?? ''}ms`;
108
+ case 'license.tierChanged':
109
+ return `${payload.from ?? '?'} → ${String(payload.to ?? '?').toUpperCase()}`;
110
+ case 'license.upgradeNudge':
111
+ return String(payload.message ?? '').slice(0, 80);
112
+ case 'install.step':
113
+ return `[${payload.status ?? ''}] ${payload.label ?? ''} (${payload.step ?? '?'}/${payload.total ?? '?'})`;
114
+ default:
115
+ return String(payload.message ?? payload.summary ?? payload.goal ?? '').slice(0, 80);
116
+ }
117
+ }
package/dist/cli.js CHANGED
@@ -1894,6 +1894,20 @@ program
1894
1894
  timeoutMs: opts.timeout ? Number(opts.timeout) : undefined,
1895
1895
  });
1896
1896
  });
1897
+ program
1898
+ .command('watch')
1899
+ .description('Tail live SSE events from the running daemon in a colored terminal feed')
1900
+ .option('--port <port>', 'Dashboard port (default: 3377 or $NEXUS_DASHBOARD_PORT)')
1901
+ .option('--all', 'Show all event types (default: mcp, tokens, planner, license, install)')
1902
+ .option('--filter <regex>', 'Custom event type filter (regex string)')
1903
+ .action(async (opts) => {
1904
+ const { runWatch } = await import('./cli/watch.js');
1905
+ await runWatch({
1906
+ port: opts.port ? Number(opts.port) : undefined,
1907
+ all: opts.all,
1908
+ filter: opts.filter,
1909
+ });
1910
+ });
1897
1911
  // Default: running `nexus-prime` with no subcommand runs `setup auto`
1898
1912
  if (process.argv.length === 2) {
1899
1913
  process.argv.push('setup', 'auto');
@@ -1,5 +1,20 @@
1
1
  import { spawn } from 'child_process';
2
+ import { readFileSync } from 'fs';
2
3
  import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ function readCliPkgVersion() {
8
+ for (const rel of ['../../package.json', '../package.json']) {
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(path.join(__dirname, rel), 'utf8'));
11
+ if (parsed.version)
12
+ return parsed.version;
13
+ }
14
+ catch { /* try next */ }
15
+ }
16
+ return 'unknown';
17
+ }
3
18
  import { acquireDaemonLock, getDaemonLockPath, isProcessAlive, readDaemonLock, removeDaemonLock, } from './lock.js';
4
19
  function sleep(ms) {
5
20
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -99,7 +114,15 @@ export async function ensureDaemonReady(workspace, options = {}) {
99
114
  const timeoutMs = options.timeoutMs ?? 15_000;
100
115
  const existing = acquireDaemonLock(workspace);
101
116
  if (existing.status === 'running') {
102
- return waitForDaemonReady(workspace, timeoutMs);
117
+ const currentVersion = readCliPkgVersion();
118
+ const daemonVersion = existing.record.pkgVersion;
119
+ if (currentVersion !== 'unknown' && daemonVersion && daemonVersion !== currentVersion) {
120
+ // Version mismatch — stop the stale daemon before spawning the new one
121
+ await stopDaemon(workspace).catch(() => { });
122
+ }
123
+ else {
124
+ return waitForDaemonReady(workspace, timeoutMs);
125
+ }
103
126
  }
104
127
  removeDaemonLock(existing.lockPath, { pid: existing.record.pid });
105
128
  if (options.spawnIfMissing === false) {
@@ -16,6 +16,8 @@ export interface DaemonLockRecord {
16
16
  stateRoot: string;
17
17
  workspaceRoot: string;
18
18
  repoRoot: string;
19
+ /** Package version of the daemon process that wrote this lock. */
20
+ pkgVersion?: string;
19
21
  }
20
22
  export interface AcquireDaemonLockResult {
21
23
  status: 'acquired' | 'running';
@@ -14,6 +14,7 @@ export declare class NexusDaemonServer {
14
14
  private heartbeatTimer;
15
15
  private stopping;
16
16
  constructor(workspace: WorkspaceContext);
17
+ private installProcessErrorHandlers;
17
18
  start(): Promise<{
18
19
  started: boolean;
19
20
  record: DaemonLockRecord;
@@ -1,6 +1,23 @@
1
1
  import http from 'http';
2
+ import { readFileSync } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
2
5
  import { randomBytes, randomUUID } from 'crypto';
3
6
  import { URL } from 'url';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ function readDaemonPkgVersion() {
10
+ for (const rel of ['../../package.json', '../package.json']) {
11
+ try {
12
+ const parsed = JSON.parse(readFileSync(path.join(__dirname, rel), 'utf8'));
13
+ if (parsed.version)
14
+ return parsed.version;
15
+ }
16
+ catch { /* try next */ }
17
+ }
18
+ return 'unknown';
19
+ }
20
+ const DAEMON_PKG_VERSION = readDaemonPkgVersion();
4
21
  import { createNexusPrime } from '../index.js';
5
22
  import { MCPAdapter } from '../agents/adapters/mcp.js';
6
23
  import { nexusEventBus } from '../engines/event-bus.js';
@@ -89,9 +106,48 @@ export class NexusDaemonServer {
89
106
  this.httpServer = http.createServer((req, res) => {
90
107
  void this.handleRequest(req, res);
91
108
  });
92
- this.cleanupTimer = setInterval(() => this.pruneState(), 60_000);
109
+ this.cleanupTimer = setInterval(() => {
110
+ try {
111
+ this.pruneState();
112
+ }
113
+ catch (err) {
114
+ // Never let cleanup crash the daemon.
115
+ console.error('[nexus-daemon] pruneState failed:', err?.message ?? err);
116
+ }
117
+ }, 60_000);
93
118
  this.cleanupTimer.unref?.();
94
119
  }
120
+ installProcessErrorHandlers() {
121
+ // Prevent a single unhandled rejection / uncaught throw from killing the
122
+ // daemon. Engines like orchestrator + SSE + synapse emit fire-and-forget
123
+ // promises; without this the first background throw takes the whole
124
+ // control plane down and orchestration starts "failing" silently for the
125
+ // client.
126
+ process.on('uncaughtException', (err) => {
127
+ try {
128
+ console.error('[nexus-daemon] uncaughtException:', err?.stack ?? err?.message ?? err);
129
+ nexusEventBus.emit('nexus.daemon.error', {
130
+ kind: 'uncaughtException',
131
+ message: err?.message ?? String(err),
132
+ stack: err?.stack ?? null,
133
+ });
134
+ }
135
+ catch { /* last-resort logging — swallow */ }
136
+ });
137
+ process.on('unhandledRejection', (reason) => {
138
+ try {
139
+ const message = reason?.message ?? String(reason);
140
+ const stack = reason?.stack ?? null;
141
+ console.error('[nexus-daemon] unhandledRejection:', stack ?? message);
142
+ nexusEventBus.emit('nexus.daemon.error', {
143
+ kind: 'unhandledRejection',
144
+ message,
145
+ stack,
146
+ });
147
+ }
148
+ catch { /* last-resort logging — swallow */ }
149
+ });
150
+ }
95
151
  async start() {
96
152
  const lock = acquireDaemonLock(this.workspace, {
97
153
  token: this.authToken,
@@ -146,6 +202,7 @@ export class NexusDaemonServer {
146
202
  port,
147
203
  token: this.authToken,
148
204
  heartbeatAt: Date.now(),
205
+ pkgVersion: DAEMON_PKG_VERSION,
149
206
  });
150
207
  // Heartbeat: keep lockfile fresh so stale-lock detection (30 s TTL) works
151
208
  // correctly after a SIGKILL. The interval is intentionally unref'd so it
@@ -165,6 +222,7 @@ export class NexusDaemonServer {
165
222
  }, HEARTBEAT_INTERVAL_MS);
166
223
  this.heartbeatTimer.unref?.();
167
224
  this.installSignalHandlers();
225
+ this.installProcessErrorHandlers();
168
226
  return {
169
227
  started: true,
170
228
  record: this.lockRecord,
@@ -228,3 +228,116 @@
228
228
  align-self: center;
229
229
  white-space: nowrap;
230
230
  }
231
+
232
+ /* ── MCP Live Activity Strip ─────────────────────────────────────────────────── */
233
+ .rt-mcp-strip {
234
+ min-height: 0;
235
+ }
236
+
237
+ .rt-mcp-strip-inner {
238
+ display: flex;
239
+ flex-wrap: wrap;
240
+ gap: 6px;
241
+ padding: 2px 0;
242
+ }
243
+
244
+ .rt-mcp-pill {
245
+ display: inline-flex;
246
+ align-items: center;
247
+ gap: 5px;
248
+ border-radius: 20px;
249
+ padding: 3px 10px 3px 7px;
250
+ font: 11px var(--font-mono, ui-monospace, monospace);
251
+ animation: rt-pill-in 0.15s ease;
252
+ white-space: nowrap;
253
+ }
254
+
255
+ .rt-mcp-active {
256
+ background: oklch(87% 0.19 152 / 8%);
257
+ border: 1px solid oklch(87% 0.19 152 / 28%);
258
+ color: var(--accent);
259
+ }
260
+
261
+ .rt-mcp-long {
262
+ background: oklch(85% 0.18 80 / 8%);
263
+ border: 1px solid oklch(85% 0.18 80 / 35%);
264
+ color: var(--warn);
265
+ }
266
+
267
+ .rt-mcp-done {
268
+ background: var(--surface);
269
+ border: 1px solid var(--border);
270
+ color: var(--text-muted);
271
+ opacity: 0.75;
272
+ }
273
+
274
+ .rt-mcp-err {
275
+ background: oklch(65% 0.22 25 / 8%);
276
+ border: 1px solid oklch(65% 0.22 25 / 25%);
277
+ color: var(--bad, #ef4444);
278
+ opacity: 0.85;
279
+ }
280
+
281
+ .rt-mcp-tool { letter-spacing: 0.01em; }
282
+ .rt-mcp-elapsed { opacity: 0.65; font-size: 10px; }
283
+
284
+ .rt-mcp-badge {
285
+ font-size: 10px;
286
+ padding: 1px 5px;
287
+ border-radius: 8px;
288
+ margin-left: 3px;
289
+ }
290
+
291
+ .rt-mcp-badge-ok { background: oklch(87% 0.19 152 / 12%); color: var(--ok, #22c55e); }
292
+ .rt-mcp-badge-err { background: oklch(65% 0.22 25 / 12%); color: var(--bad, #ef4444); }
293
+
294
+ /* ── Toast system ─────────────────────────────────────────────────────────────── */
295
+ .rt-toast-container {
296
+ position: fixed;
297
+ bottom: 24px;
298
+ right: 24px;
299
+ display: flex;
300
+ flex-direction: column-reverse;
301
+ gap: 8px;
302
+ z-index: 9999;
303
+ pointer-events: none;
304
+ }
305
+
306
+ .rt-toast {
307
+ padding: 10px 16px;
308
+ border-radius: 8px;
309
+ font: 13px ui-sans-serif, system-ui, sans-serif;
310
+ max-width: 360px;
311
+ box-shadow: 0 4px 16px oklch(0% 0 0 / 40%);
312
+ animation: rt-toast-in 0.2s ease;
313
+ pointer-events: auto;
314
+ transition: opacity 0.4s;
315
+ }
316
+
317
+ .rt-toast-info { background: var(--surface); border: 1px solid var(--accent); color: var(--text); }
318
+ .rt-toast-warn { background: var(--surface); border: 1px solid var(--warn); color: var(--warn); }
319
+ .rt-toast-bad { background: var(--surface); border: 1px solid var(--bad, #ef4444); color: var(--bad, #ef4444); }
320
+
321
+ .rt-toast-fade { opacity: 0; }
322
+
323
+ @keyframes rt-toast-in {
324
+ from { opacity: 0; transform: translateY(8px); }
325
+ to { opacity: 1; transform: none; }
326
+ }
327
+
328
+ /* ── Upgrade nudge bar ───────────────────────────────────────────────────────── */
329
+ .rt-upgrade-bar {
330
+ display: flex;
331
+ align-items: center;
332
+ gap: 12px;
333
+ background: oklch(85% 0.18 80 / 8%);
334
+ border: 1px solid oklch(85% 0.18 80 / 30%);
335
+ border-radius: 8px;
336
+ padding: 10px 14px;
337
+ margin-bottom: 14px;
338
+ font-size: 13px;
339
+ }
340
+
341
+ .rt-upgrade-msg { flex: 1; color: var(--warn); }
342
+ .rt-upgrade-btn { padding: 4px 12px; border-radius: 6px; background: var(--warn); color: #000; font-weight: 600; font-size: 12px; text-decoration: none; }
343
+ .rt-upgrade-dismiss { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 14px; padding: 0 4px; }