nexus-prime 7.1.1 → 7.2.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.
@@ -293,7 +293,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
293
293
  detailLevel === 'debug' && bootstrap.tokenOptimization?.autoApplied && bootstrap.tokenOptimization?.plan
294
294
  ? `Auto token plan\n\`\`\`txt\n${bootstrap.tokenOptimization.plan}\n\`\`\``
295
295
  : '',
296
- detailLevel === 'debug' ? formatJsonDetails('Structured details', payload) : '',
296
+ formatJsonDetails('Structured details', payload),
297
297
  hctx.formatProtocolChecklist(),
298
298
  ].filter(Boolean).join('\n\n'),
299
299
  }],
@@ -499,6 +499,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
499
499
  `Verification: ${verifiedWorkers}/${execution.workerResults.length} worker(s) verified`,
500
500
  `Tokens: saved ${Number(execution.tokenTelemetry?.savedTokens || 0).toLocaleString()} · compression ${Number(execution.tokenTelemetry?.compressionPct || 0)}%`,
501
501
  autoTokenApplyNote || null,
502
+ `Payload ref: ${workspace.stateKey} · ${detailLevel}`,
502
503
  ...(detailLevel === 'debug' ? [
503
504
  `Specialists: ${execution.plannerState?.selectedSpecialists.map((specialist) => specialist.name).slice(0, 4).join(', ') || 'none selected'}`,
504
505
  `Assets: ${(execution.activeSkills || []).length} skills · ${(execution.activeWorkflows || []).length} workflows · ${(runtimeUsage.artifactSelectionAudit?.selected?.length || 0)} audited selections`,
@@ -512,7 +513,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
512
513
  ] : []),
513
514
  ]),
514
515
  detailLevel === 'debug' && execution.result ? `Result\n\`\`\`\n${execution.result}\n\`\`\`` : `Result preview\n\`\`\`\n${truncateText(execution.result || summarizeExecution(execution), 900)}\n\`\`\``,
515
- detailLevel === 'debug' ? formatJsonDetails('Structured details', payload) : '',
516
+ formatJsonDetails('Structured details', payload),
516
517
  hctx.formatRemainingProtocolSteps(),
517
518
  ].filter(Boolean).join('\n\n'),
518
519
  }],
@@ -1,22 +1,15 @@
1
1
  /**
2
2
  * nexus cleanup — bound the growth of worktree + runs directories.
3
3
  *
4
- * Different from `uninstall`: this never touches persistent state. It enforces
5
- * TTL / count / bytes budgets over transient scratch dirs so local disk usage
6
- * can't balloon without an operator noticing.
7
- *
8
- * Defaults chosen to be loose enough that a busy day of work doesn't trip them:
9
- * - TTL: 12h per worktree, 6h per run
10
- * - maxCount: 32 worktrees, 64 runs
11
- * - maxBytes: 2 GiB across worktrees, 1 GiB across runs
12
- *
13
- * Override with env: NEXUS_WORKTREE_TTL_MS, NEXUS_WORKTREE_MAX_COUNT,
14
- * NEXUS_WORKTREE_MAX_BYTES, NEXUS_RUNS_TTL_MS, NEXUS_RUNS_MAX_COUNT,
15
- * NEXUS_RUNS_MAX_BYTES.
4
+ * This never touches durable memory. It enforces aggressive defaults because
5
+ * transient Nexus state must never surprise users with 10GB+ growth. Heavy
6
+ * artifacts can still be retained by raising env budgets explicitly.
16
7
  */
8
+ import * as fs from 'fs';
9
+ import * as os from 'os';
17
10
  import * as path from 'path';
18
11
  import { formatBytes, sweepDirectory, sweepOrphanWorktrees, } from '../install/fs-purge.js';
19
- import { getNexusStateDir, getWorktreeRoots, getRuntimeTmpRoots, } from '../install/state-locator.js';
12
+ import { getNexusStateDir, getWorktreeRoots, getRuntimeTmpRoots, NEXUS_TMP_GLOB_PREFIXES, } from '../install/state-locator.js';
20
13
  function parseIntEnv(name, fallback) {
21
14
  const raw = process.env[name];
22
15
  const parsed = Number.parseInt(raw ?? '', 10);
@@ -24,18 +17,42 @@ function parseIntEnv(name, fallback) {
24
17
  }
25
18
  export function resolveWorktreeBudget() {
26
19
  return {
27
- ttlMs: parseIntEnv('NEXUS_WORKTREE_TTL_MS', 12 * 60 * 60 * 1000),
28
- maxCount: parseIntEnv('NEXUS_WORKTREE_MAX_COUNT', 32),
29
- maxBytes: parseIntEnv('NEXUS_WORKTREE_MAX_BYTES', 2 * 1024 * 1024 * 1024),
20
+ ttlMs: parseIntEnv('NEXUS_WORKTREE_TTL_MS', 60 * 60 * 1000),
21
+ maxCount: parseIntEnv('NEXUS_WORKTREE_MAX_COUNT', 8),
22
+ maxBytes: parseIntEnv('NEXUS_WORKTREE_MAX_BYTES', 512 * 1024 * 1024),
30
23
  };
31
24
  }
32
25
  export function resolveRunsBudget() {
33
26
  return {
34
- ttlMs: parseIntEnv('NEXUS_RUNS_TTL_MS', 6 * 60 * 60 * 1000),
35
- maxCount: parseIntEnv('NEXUS_RUNS_MAX_COUNT', 64),
36
- maxBytes: parseIntEnv('NEXUS_RUNS_MAX_BYTES', 1024 * 1024 * 1024),
27
+ ttlMs: parseIntEnv('NEXUS_RUNS_TTL_MS', 30 * 60 * 1000),
28
+ maxCount: parseIntEnv('NEXUS_RUNS_MAX_COUNT', 16),
29
+ maxBytes: parseIntEnv('NEXUS_RUNS_MAX_BYTES', 256 * 1024 * 1024),
37
30
  };
38
31
  }
32
+ function collectLegacyTmpRoots() {
33
+ const tmpRoot = os.tmpdir();
34
+ let entries = [];
35
+ try {
36
+ entries = fs.readdirSync(tmpRoot);
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ const roots = new Set();
42
+ for (const entry of entries) {
43
+ if (!NEXUS_TMP_GLOB_PREFIXES.some((prefix) => entry.startsWith(prefix)))
44
+ continue;
45
+ const fullPath = path.join(tmpRoot, entry);
46
+ try {
47
+ if (fs.statSync(fullPath).isDirectory())
48
+ roots.add(fullPath);
49
+ }
50
+ catch {
51
+ // ignore races with concurrently removed tmp dirs
52
+ }
53
+ }
54
+ return [...roots].sort();
55
+ }
39
56
  export async function runCleanup(options) {
40
57
  const dryRun = options.dryRun || !options.fix;
41
58
  const repoRoot = options.repoRoot ?? process.cwd();
@@ -51,7 +68,6 @@ export async function runCleanup(options) {
51
68
  };
52
69
  const label = dryRun ? '[dry-run]' : '[cleanup]';
53
70
  console.log(`\n${label} Nexus cleanup — repo: ${repoRoot}`);
54
- // Worktree roots
55
71
  for (const root of getWorktreeRoots()) {
56
72
  const sweep = sweepDirectory(root, {
57
73
  maxAgeMs: worktreeBudget.ttlMs,
@@ -65,6 +81,7 @@ export async function runCleanup(options) {
65
81
  report.totalEntriesRemoved += sweep.removed.length;
66
82
  if (sweep.entriesBefore > 0 || sweep.removed.length > 0) {
67
83
  console.log(` worktree: ${root}`);
84
+ console.log(` budget: ttl=${Math.round(worktreeBudget.ttlMs / 60000)}m, max=${worktreeBudget.maxCount}, bytes=${formatBytes(worktreeBudget.maxBytes)}`);
68
85
  console.log(` before: ${sweep.entriesBefore} entries, ${formatBytes(sweep.totalBytesBefore)}`);
69
86
  console.log(` removed: ${sweep.removed.length} entries, ${formatBytes(sweep.removed.reduce((s, r) => s + r.bytes, 0))}`);
70
87
  for (const r of sweep.removed) {
@@ -72,8 +89,12 @@ export async function runCleanup(options) {
72
89
  }
73
90
  }
74
91
  }
75
- // Runs roots
76
- for (const root of [path.join(getNexusStateDir(), 'runs'), ...getRuntimeTmpRoots()]) {
92
+ const runsRoots = new Set([
93
+ path.join(getNexusStateDir(), 'runs'),
94
+ ...getRuntimeTmpRoots(),
95
+ ...collectLegacyTmpRoots(),
96
+ ]);
97
+ for (const root of runsRoots) {
77
98
  const sweep = sweepDirectory(root, {
78
99
  maxAgeMs: runsBudget.ttlMs,
79
100
  maxCount: runsBudget.maxCount,
@@ -86,11 +107,11 @@ export async function runCleanup(options) {
86
107
  report.totalEntriesRemoved += sweep.removed.length;
87
108
  if (sweep.entriesBefore > 0 || sweep.removed.length > 0) {
88
109
  console.log(` runs: ${root}`);
110
+ console.log(` budget: ttl=${Math.round(runsBudget.ttlMs / 60000)}m, max=${runsBudget.maxCount}, bytes=${formatBytes(runsBudget.maxBytes)}`);
89
111
  console.log(` before: ${sweep.entriesBefore} entries, ${formatBytes(sweep.totalBytesBefore)}`);
90
112
  console.log(` removed: ${sweep.removed.length} entries, ${formatBytes(sweep.removed.reduce((s, r) => s + r.bytes, 0))}`);
91
113
  }
92
114
  }
93
- // Orphan worktrees (dir exists on disk but `git worktree list` forgot about it)
94
115
  try {
95
116
  const orphanReport = await sweepOrphanWorktrees(repoRoot, { dryRun });
96
117
  report.orphans.paths = orphanReport.ops.filter((op) => op.kind !== 'missing').map((op) => op.path);
@@ -2,17 +2,12 @@ import { loadManifest } from '../install/manifest.js';
2
2
  import { type StopAllNexusProcessesOptions } from '../install/process-cleanup.js';
3
3
  export interface UninstallOptions {
4
4
  dryRun: boolean;
5
- /** Stop daemons and unregister all known clients. Accepted for CLI clarity; uninstall already does this by default. */
6
5
  all?: boolean;
7
- /** Preferred purge flag. */
8
6
  purge?: boolean;
9
- /** Back-compat alias for purge. */
10
7
  purgeAll?: boolean;
11
8
  yes: boolean;
12
9
  workspaceRoot?: string;
13
- /** Skip interactive prompt in tests. */
14
10
  _forceYes?: boolean;
15
- /** Test-only process cleanup injection; production callers should not set this. */
16
11
  _processCleanup?: StopAllNexusProcessesOptions;
17
12
  }
18
13
  export interface UninstallReport {
@@ -1,14 +1,10 @@
1
1
  /**
2
2
  * nexus uninstall — reverse every mutation Nexus Prime made to this machine.
3
3
  *
4
- * `--dry-run` walks the install manifest + heuristics and prints what would
5
- * happen without touching anything.
6
- * `--purge` additionally removes persistent state (memory.db, synapse.db,
7
- * architects.db, logs, caches, worktrees). Requires `--yes`.
8
- *
9
- * The manifest is authoritative where present. Heuristics fill the gap when
10
- * the manifest is missing (e.g. legacy installs). Every destructive step has
11
- * a dry-run preview, and leftovers are always reported.
4
+ * `--dry-run` walks registrations, processes, markers, and filesystem paths.
5
+ * Default uninstall unregisters clients, stops processes, clears markers, removes
6
+ * transient scratch, and clears the install manifest. `--purge` also deletes
7
+ * durable state such as memory, Synapse, Architects, logs, caches, and worktrees.
12
8
  */
13
9
  import * as fs from 'fs';
14
10
  import * as readline from 'readline';
@@ -73,7 +69,6 @@ export async function runUninstall(options) {
73
69
  }
74
70
  const label = dryRun ? '[dry-run]' : '[uninstall]';
75
71
  console.log(`\n${label} Nexus Prime uninstall — workspace: ${workspaceRoot}`);
76
- // Step 1 — reverse IDE registrations before stopping processes so clients do not immediately respawn MCP sessions.
77
72
  const manifest = loadManifest();
78
73
  const unregisterReport = unregisterNexusRegistrations({ workspaceRoot, dryRun, manifest });
79
74
  const removedRegs = unregisterReport.reports.filter((r) => r.removed);
@@ -83,13 +78,11 @@ export async function runUninstall(options) {
83
78
  for (const r of removedRegs) {
84
79
  console.log(` - ${r.filePath} :: ${r.jsonPath.join('.') || '(root)'}`);
85
80
  }
86
- // Step 2 — stop Nexus MCP/daemon processes and remove lockfiles.
87
81
  const processReport = await stopAllNexusProcesses({ ...options._processCleanup, dryRun });
88
82
  report.daemonsKilled = processReport.daemonsKilled;
89
83
  report.lockfilesRemoved = processReport.lockfilesRemoved;
90
84
  report.errors.push(...processReport.errors);
91
85
  console.log(` ${dryRun ? '◦' : '✓'} processes: ${report.daemonsKilled} process(es), ${report.lockfilesRemoved} lockfile(s)`);
92
- // Step 3 — remove setup markers.
93
86
  const markers = new Set([setupMarkerPath, ...manifest.setupMarkers]);
94
87
  for (const marker of markers) {
95
88
  if (!fs.existsSync(marker))
@@ -107,7 +100,6 @@ export async function runUninstall(options) {
107
100
  }
108
101
  }
109
102
  }
110
- // Step 4 — purge filesystem artifacts.
111
103
  const extras = manifest.paths.map((e) => ({ path: e.path, scope: e.scope }));
112
104
  let stateReport = null;
113
105
  let tmpReport = null;
@@ -116,7 +108,6 @@ export async function runUninstall(options) {
116
108
  tmpReport = purgeTmpRoots({ dryRun });
117
109
  }
118
110
  else {
119
- // Default (no --purge-all): only transient tmp dirs. Persistent state stays.
120
111
  tmpReport = purgeTmpRoots({ dryRun });
121
112
  }
122
113
  const totals = [stateReport, tmpReport]
@@ -137,11 +128,9 @@ export async function runUninstall(options) {
137
128
  console.log(` - ${op.path} (${formatBytes(op.bytes)})`);
138
129
  }
139
130
  }
140
- // Step 5 — final manifest clear, but only on full uninstall, not dry-run.
141
- if (!dryRun && purgeAll) {
131
+ if (!dryRun) {
142
132
  clearManifest(installManifestPath);
143
133
  }
144
- // Step 6 — leftover audit.
145
134
  report.leftovers = collectLeftovers(nexusStateDir);
146
135
  if (report.leftovers.length > 0) {
147
136
  console.log(` ! leftovers (still present in state dir):`);
@@ -156,7 +145,8 @@ export async function runUninstall(options) {
156
145
  report.ok = report.errors.length === 0 && (dryRun || (purgeAll ? report.leftovers.length === 0 : true));
157
146
  console.log(`\n${label} done. ${report.ok ? 'OK' : 'completed with issues'}.`);
158
147
  if (!purgeAll && !dryRun) {
159
- console.log(' Persistent state preserved. Re-run with --purge --yes to remove everything.');
148
+ console.log(' Persistent memory preserved. All registrations, processes, manifest, and transient scratch were removed.');
149
+ console.log(' Re-run with --purge --yes to delete durable memory/Synapse/Architects state too.');
160
150
  }
161
151
  return report;
162
152
  }
@@ -6,7 +6,7 @@ import { SessionDNAManager } from './session-dna.js';
6
6
  import { clearBootstrapReceipt, readBootstrapReceipt } from './bootstrap/bootstrap-registry.js';
7
7
  import { doctorGitWorktrees } from './worktree-health.js';
8
8
  import { sweepDirectory, sweepOrphanWorktrees } from '../install/fs-purge.js';
9
- import { getWorktreeRoots } from '../install/state-locator.js';
9
+ import { getRuntimeTmpRoots, getWorktreeRoots } from '../install/state-locator.js';
10
10
  import { resolveWorktreeBudget, resolveRunsBudget } from '../cli/cleanup.js';
11
11
  function isOlderThan(target, maxAgeMs) {
12
12
  try {
@@ -59,15 +59,12 @@ function cleanupTmpDirEntries(rootDir, opts) {
59
59
  export async function runStartupHygiene(input) {
60
60
  const mode = input.mode ?? 'stale';
61
61
  const stateDir = resolveNexusStateDir();
62
- // 1) Runtime registry snapshots (dashboard + operators) — safe to prune stale/dead entries.
63
62
  const registryDir = path.join(stateDir, 'runtime-registry');
64
63
  const registryBefore = fs.existsSync(registryDir) ? fs.readdirSync(registryDir).filter((f) => f.endsWith('.json')).length : 0;
65
64
  const registry = new RuntimeRegistry(stateDir);
66
65
  registry.pruneStalePublic();
67
66
  const registryAfter = fs.existsSync(registryDir) ? fs.readdirSync(registryDir).filter((f) => f.endsWith('.json')).length : 0;
68
- // 2) SessionDNA stale locks + orphan session JSONs (best-effort).
69
67
  const cleanedSessionLocks = SessionDNAManager.cleanStale(path.join(stateDir, 'sessions'));
70
- // 3) Bootstrap receipt — safe to clear when stale/invalid (never deletes installed configs).
71
68
  let clearedBootstrapReceipt = false;
72
69
  if (mode === 'fresh') {
73
70
  clearBootstrapReceipt(input.workspaceStateRoot);
@@ -75,40 +72,32 @@ export async function runStartupHygiene(input) {
75
72
  }
76
73
  else {
77
74
  const receipt = readBootstrapReceipt(input.workspaceStateRoot);
78
- // If the receipt is stale or corrupt, readBootstrapReceipt returns null; clear the file to avoid confusing future probes.
79
75
  if (receipt === null) {
80
76
  clearBootstrapReceipt(input.workspaceStateRoot);
81
77
  clearedBootstrapReceipt = true;
82
78
  }
83
79
  }
84
- // 4) Git worktree metadata — only prunes Nexus-owned broken/stale entries.
85
80
  let worktreeDoctorOverall;
86
81
  try {
87
82
  const health = await doctorGitWorktrees(input.repoRoot);
88
83
  worktreeDoctorOverall = health.overall;
89
84
  }
90
85
  catch {
91
- // ignore — hygiene must not block startup
86
+ // hygiene must not block startup
92
87
  }
93
- // 5) Temp artifacts (transient only). Never touches memory DB / vault.
94
88
  const tmpDir = os.tmpdir();
95
- const maxAgeMs = 2 * 60 * 60 * 1000;
89
+ const maxAgeMs = mode === 'fresh' ? 0 : 30 * 60 * 1000;
96
90
  let cleanedTmpEntries = 0;
97
- cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-runs'), { mode, maxAgeMs });
98
- cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-worktrees'), { mode, maxAgeMs });
99
- cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-runtime-skills'), { mode, maxAgeMs });
100
- cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-runtime-workflows'), { mode, maxAgeMs });
101
- cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-runtime-hooks'), { mode, maxAgeMs });
102
- cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-runtime-automations'), { mode, maxAgeMs });
103
- cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-sessions'), { mode, maxAgeMs });
91
+ const transientRoots = [...getRuntimeTmpRoots(), ...getWorktreeRoots()];
92
+ for (const root of transientRoots) {
93
+ cleanedTmpEntries += cleanupTmpDirEntries(root, { mode, maxAgeMs });
94
+ }
104
95
  cleanedTmpEntries += cleanupTmpDirEntries(tmpDir, { mode, maxAgeMs, matchPrefix: 'nexus-prime-feature-registry-' });
96
+ cleanedTmpEntries += cleanupTmpDirEntries(tmpDir, { mode, maxAgeMs, matchPrefix: 'nexus-reset-test-' });
105
97
  if (mode === 'fresh') {
106
- // Optional fresh wipe of purely transient runtime UI/event buffers.
107
98
  cleanedTmpEntries += safeUnlink(path.join(stateDir, 'events.jsonl')) ? 1 : 0;
108
99
  cleanedTmpEntries += safeUnlink(path.join(stateDir, 'dashboard-state.json')) ? 1 : 0;
109
100
  }
110
- // 6) Bounded worktree sweep — enforce TTL / count / bytes budgets so growth
111
- // stays bounded even when users never run `nexus cleanup`.
112
101
  const worktreeBudget = resolveWorktreeBudget();
113
102
  let boundedWorktreeSweeps = 0;
114
103
  for (const root of getWorktreeRoots()) {
@@ -123,34 +112,33 @@ export async function runStartupHygiene(input) {
123
112
  boundedWorktreeSweeps += sweep.removed.length;
124
113
  }
125
114
  catch {
126
- // sweep is best-effort
115
+ // best-effort
127
116
  }
128
117
  }
129
- // 7) Orphan worktree sweep — dirs exist on disk but git forgot them.
130
118
  let orphanWorktreesRemoved = 0;
131
119
  try {
132
120
  const orphans = await sweepOrphanWorktrees(input.repoRoot, { dryRun: false });
133
121
  orphanWorktreesRemoved = orphans.ops.filter((op) => op.removed).length;
134
122
  }
135
123
  catch {
136
- // ignore — orphan sweep must not block startup
124
+ // ignore
137
125
  }
138
- // 8) Bounded runs sweep.
139
126
  const runsBudget = resolveRunsBudget();
140
127
  let boundedRunsSweeps = 0;
141
- try {
142
- const runsRoot = path.join(input.workspaceStateRoot, 'runs');
143
- const sweep = sweepDirectory(runsRoot, {
144
- maxAgeMs: runsBudget.ttlMs,
145
- maxCount: runsBudget.maxCount,
146
- maxBytes: runsBudget.maxBytes,
147
- dryRun: false,
148
- keepRoot: true,
149
- });
150
- boundedRunsSweeps += sweep.removed.length;
151
- }
152
- catch {
153
- // best-effort
128
+ for (const runsRoot of [path.join(input.workspaceStateRoot, 'runs'), ...getRuntimeTmpRoots()]) {
129
+ try {
130
+ const sweep = sweepDirectory(runsRoot, {
131
+ maxAgeMs: runsBudget.ttlMs,
132
+ maxCount: runsBudget.maxCount,
133
+ maxBytes: runsBudget.maxBytes,
134
+ dryRun: false,
135
+ keepRoot: true,
136
+ });
137
+ boundedRunsSweeps += sweep.removed.length;
138
+ }
139
+ catch {
140
+ // best-effort
141
+ }
154
142
  }
155
143
  return {
156
144
  mode,
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * process-cleanup — stop every Nexus-owned process without touching anything else.
3
3
  *
4
- * Wraps `runPostinstallCleanup` (which handles the per-workspace daemon lock)
5
- * and adds a final scan for lockfiles across the entire state dir in case a
6
- * workspace-local daemon was started from a now-deleted path.
4
+ * Wraps lockfile cleanup and adds a process-table sweep for legacy launch
5
+ * shapes. Older installs can be started by npx/npm, by MCP clients as
6
+ * `node .../dist/index.js`, or by dashboard/daemon commands that never wrote a
7
+ * current lockfile. Uninstall must catch all of those, otherwise users think
8
+ * Nexus was removed while stale workers keep writing state.
7
9
  */
8
10
  import * as fs from 'fs';
9
11
  import { spawnSync } from 'child_process';
@@ -36,30 +38,43 @@ export function killProcessTree(pid) {
36
38
  function sleep(ms) {
37
39
  return new Promise((resolve) => setTimeout(resolve, ms));
38
40
  }
41
+ function normalizeCommand(command) {
42
+ return command.replace(/\s+/g, ' ').trim();
43
+ }
44
+ function looksLikeNexusEntrypoint(command) {
45
+ const normalized = normalizeCommand(command);
46
+ return /\bnexus-prime(?:\s|$)/.test(normalized)
47
+ || /(?:^|[\s"'])npx(?:\.cmd)?(?:\s+--yes)?\s+nexus-prime(?:\s|$)/.test(normalized)
48
+ || /(?:^|[\s"'])npm(?:\.cmd)?\s+(?:exec|x)\s+(?:--\s+)?nexus-prime(?:\s|$)/.test(normalized)
49
+ || /[\\/]nexus-prime[\\/](?:dist[\\/])?(?:index|cli)\.js(?:\s|$)/.test(normalized)
50
+ || /[\\/]node_modules[\\/]\.bin[\\/]nexus-prime(?:\s|$)/.test(normalized);
51
+ }
52
+ function isBenignNexusCommand(command) {
53
+ const normalized = normalizeCommand(command);
54
+ return /\b(npm|pnpm|yarn)\s+(?:install|add|remove|uninstall|view|info|show)\b/.test(normalized)
55
+ || /\b(vitest|jest|tsc|tsx)\b.*\bsrc[\\/]install[\\/]process-cleanup\.ts\b/.test(normalized);
56
+ }
39
57
  function isNexusProcessCommand(command) {
40
- return /\bnexus-prime(?:\s|$)/.test(command)
41
- && (/\bmcp(?:\s|$)/.test(command)
42
- || /\bdaemon\s+__serve(?:\s|$)/.test(command));
58
+ const normalized = normalizeCommand(command);
59
+ if (!looksLikeNexusEntrypoint(normalized) || isBenignNexusCommand(normalized))
60
+ return false;
61
+ // Runtime surfaces that can keep writing state / holding dashboard ports.
62
+ if (/\b(mcp|daemon\s+__serve|dashboard|server|serve|start|orchestrate|bootstrap)\b/.test(normalized)) {
63
+ return true;
64
+ }
65
+ // MCP clients often launch the compiled entrypoint without a visible subcommand.
66
+ return /[\\/]nexus-prime[\\/]dist[\\/](?:index|cli)\.js(?:\s|$)/.test(normalized)
67
+ || /[\\/]node_modules[\\/]\.bin[\\/]nexus-prime(?:\s|$)/.test(normalized);
43
68
  }
44
- export function listNexusProcessPids() {
45
- if (process.platform === 'win32') {
46
- return [];
47
- }
48
- const result = spawnSync('ps', ['-axo', 'pid=,command='], {
49
- encoding: 'utf8',
50
- timeout: 5_000,
51
- });
52
- if (result.status !== 0) {
53
- return [];
54
- }
69
+ function parseProcessLines(stdout) {
55
70
  const candidates = [];
56
- for (const line of (result.stdout ?? '').split(/\r?\n/)) {
57
- const match = line.match(/^\s*(\d+)\s+(.+)$/);
71
+ for (const line of (stdout ?? '').split(/\r?\n/)) {
72
+ const match = line.match(/^\s*(\d+)\s+(.+)$/) ?? line.match(/^\s*(\d+)\t(.+)$/);
58
73
  if (!match)
59
74
  continue;
60
75
  const pid = Number(match[1]);
61
76
  const command = match[2];
62
- if (pid === process.pid || !Number.isInteger(pid) || pid <= 0)
77
+ if (pid === process.pid || pid === process.ppid || !Number.isInteger(pid) || pid <= 1)
63
78
  continue;
64
79
  if (!isNexusProcessCommand(command))
65
80
  continue;
@@ -67,6 +82,28 @@ export function listNexusProcessPids() {
67
82
  }
68
83
  return candidates;
69
84
  }
85
+ export function listNexusProcessPids() {
86
+ if (process.platform === 'win32') {
87
+ const result = spawnSync('powershell.exe', [
88
+ '-NoProfile',
89
+ '-Command',
90
+ "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'nexus-prime|nexus-prime.*dist|node_modules.*nexus-prime' } | ForEach-Object { \"$($_.ProcessId)`t$($_.CommandLine)\" }",
91
+ ], {
92
+ encoding: 'utf8',
93
+ timeout: 5_000,
94
+ windowsHide: true,
95
+ });
96
+ return result.status === 0 ? parseProcessLines(result.stdout ?? '') : [];
97
+ }
98
+ const result = spawnSync('ps', ['-axo', 'pid=,command='], {
99
+ encoding: 'utf8',
100
+ timeout: 5_000,
101
+ });
102
+ if (result.status !== 0) {
103
+ return [];
104
+ }
105
+ return parseProcessLines(result.stdout ?? '');
106
+ }
70
107
  async function stopPid(pid, options) {
71
108
  if (options.dryRun) {
72
109
  return { stopped: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.1.1",
3
+ "version": "7.2.0",
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",