mcp-nervous-system 1.5.2 → 1.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.
package/index.js CHANGED
@@ -5,11 +5,62 @@ const crypto = require('crypto');
5
5
  const { spawn } = require('child_process');
6
6
  const fs = require('fs');
7
7
 
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ // ============================================================
12
+ // PROJECT CONFIGURATION - Auto-discover or use config file
13
+ // ============================================================
14
+
15
+ function loadProjectConfig() {
16
+ const configPaths = [
17
+ process.env.NS_CONFIG_PATH,
18
+ path.join(process.cwd(), 'nervous-system.config.json'),
19
+ path.join(os.homedir(), '.nervous-system', 'config.json'),
20
+ path.join(__dirname, 'nervous-system.config.json'),
21
+ ].filter(Boolean);
22
+
23
+ for (const cp of configPaths) {
24
+ try {
25
+ const raw = fs.readFileSync(cp, 'utf8');
26
+ const cfg = JSON.parse(raw);
27
+ cfg._source = cp;
28
+ return cfg;
29
+ } catch (e) { continue; }
30
+ }
31
+
32
+ // Return defaults that work for any project
33
+ return {
34
+ _source: 'defaults',
35
+ project_root: process.cwd(),
36
+ data_dir: null,
37
+ logs_dir: null,
38
+ html_dir: null,
39
+ protected_files_list: null,
40
+ config_file: null,
41
+ roles_file: null,
42
+ docs_to_audit: [],
43
+ pm2_managed: false,
44
+ html_pages: [],
45
+ package_json: null,
46
+ github_repo: null,
47
+ };
48
+ }
49
+
50
+ const PROJECT = loadProjectConfig();
51
+
52
+ function projectPath(key) {
53
+ const val = PROJECT[key];
54
+ if (!val) return null;
55
+ if (path.isAbsolute(val)) return val;
56
+ return path.join(PROJECT.project_root || process.cwd(), val);
57
+ }
58
+
8
59
  const PORT = 3475;
9
60
 
10
61
  const KILL_SECRET = process.env.KILL_SECRET || 'ns-kill-2026';
11
- const AUDIT_CHAIN_FILE = '/root/family-data/audit-chain.json';
12
- const VIOLATIONS_LOG = '/root/family-logs/guardrail-violations.log';
62
+ const AUDIT_CHAIN_FILE = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'audit-chain.json') : path.join(os.homedir(), '.nervous-system', 'audit-chain.json');
63
+ const VIOLATIONS_LOG = projectPath('logs_dir') ? path.join(projectPath('logs_dir'), 'guardrail-violations.log') : path.join(os.homedir(), '.nervous-system', 'guardrail-violations.log');
13
64
  const GENESIS_HASH = '0000000000000000000000000000000000000000000000000000000000000000';
14
65
  const activeDispatches = [];
15
66
  const MAX_CONCURRENT_DISPATCHES = 2;
@@ -108,7 +159,7 @@ function dispatchToLLM(task, maxTurns) {
108
159
  const freeMB = getFreeMB();
109
160
  if (freeMB < 500) return { dispatched: false, error: `Insufficient RAM: ${freeMB}MB free (need 500MB+)` };
110
161
  const ts = Date.now();
111
- const logFile = `/root/family-logs/dispatch-${ts}.log`;
162
+ const logFile = projectPath('logs_dir') ? `${projectPath('logs_dir')}/dispatch-${ts}.log` : path.join(os.homedir(), '.nervous-system', `dispatch-${ts}.log`);
112
163
  const turns = maxTurns || 15;
113
164
  try {
114
165
  const escaped = task.replace(/"/g, '\\"');
@@ -129,7 +180,7 @@ const MCP_VERSION = '2024-11-05';
129
180
  // Server info
130
181
  const SERVER_INFO = {
131
182
  name: 'nervous-system',
132
- version: '1.5.1'
183
+ version: '1.6.0'
133
184
  };
134
185
 
135
186
  // ============================================================
@@ -138,7 +189,7 @@ const SERVER_INFO = {
138
189
 
139
190
  const FRAMEWORK = {
140
191
  name: 'The Nervous System',
141
- version: '1.5.1',
192
+ version: '1.6.0',
142
193
  author: 'Arthur Palyan',
143
194
  tagline: 'Anthropic built the brain. Arthur built the nervous system that keeps it from hurting itself.',
144
195
  problem: 'LLMs lose context between sessions, loop on problems instead of dispatching, silently fail without progress notes, edit protected files, drift from the real problem, and solve instead of asking.',
@@ -649,6 +700,21 @@ const TOOLS = [
649
700
  page: { type: 'string', description: "Specific page to check (e.g. 'gateway.html'), or 'all' for everything" }
650
701
  }
651
702
  }
703
+ },
704
+ // NEW: Pre-Publish Audit
705
+ {
706
+ name: 'pre_publish_audit',
707
+ annotations: { title: 'Pre-Publish Audit', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
708
+ description: 'Scans the Nervous System source code itself before publishing. Catches hardcoded secrets, personal data, non-portable paths, and internal naming that should not ship to clients. RUN THIS BEFORE EVERY npm publish.',
709
+ inputSchema: {
710
+ type: 'object',
711
+ properties: {
712
+ source_file: {
713
+ type: 'string',
714
+ description: 'Path to the NS source file to audit. Defaults to own index.js'
715
+ }
716
+ }
717
+ }
652
718
  }
653
719
  ];
654
720
 
@@ -686,7 +752,10 @@ function safeReadFile(filePath) {
686
752
  function auditRoles() {
687
753
  const drifts = [];
688
754
  let cleanChecks = 0;
689
- const rolesFile = '/root/family-data/family-roles.json';
755
+ const rolesFile = projectPath('roles_file');
756
+ if (!rolesFile) {
757
+ return { drifts: [], cleanChecks: 0, skipped: 'roles_file not configured' };
758
+ }
690
759
  const roles = safeReadJSON(rolesFile);
691
760
  if (!roles || !roles.members) {
692
761
  drifts.push({ type: 'missing_source', source: rolesFile, target: '', field: '', expected: 'valid JSON with members array', found: 'missing or invalid' });
@@ -699,7 +768,8 @@ function auditRoles() {
699
768
  }
700
769
 
701
770
  // Check family-status.json
702
- const statusFile = '/root/family-data/family-status.json';
771
+ const statusFile = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'family-status.json') : null;
772
+ if (!statusFile) { return { drifts, cleanChecks }; }
703
773
  const status = safeReadJSON(statusFile);
704
774
  if (status && status.members) {
705
775
  for (const m of status.members) {
@@ -715,8 +785,8 @@ function auditRoles() {
715
785
  }
716
786
 
717
787
  // Check system-config.json
718
- const configFile = '/root/family-data/system-config.json';
719
- const config = safeReadJSON(configFile);
788
+ const configFile = projectPath('config_file');
789
+ const config = configFile ? safeReadJSON(configFile) : null;
720
790
  if (config && config.family_members) {
721
791
  for (const m of config.family_members) {
722
792
  const src = sourceRoles[m.id];
@@ -731,8 +801,8 @@ function auditRoles() {
731
801
  }
732
802
 
733
803
  // Check family-guide.json
734
- const guideFile = '/root/family-data/family-guide.json';
735
- const guide = safeReadJSON(guideFile);
804
+ const guideFile = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'family-guide.json') : null;
805
+ const guide = guideFile ? safeReadJSON(guideFile) : null;
736
806
  if (guide && guide.members) {
737
807
  for (const m of guide.members) {
738
808
  const src = sourceRoles[m.id];
@@ -747,11 +817,12 @@ function auditRoles() {
747
817
  }
748
818
 
749
819
  // Check HTML files for role references
750
- const htmlFiles = [
751
- { path: '/root/family-home/index.html', name: 'index.html' },
752
- { path: '/root/family-home/explorer.html', name: 'explorer.html' },
753
- { path: '/root/family-home/meet.html', name: 'meet.html' }
754
- ];
820
+ const htmlDir = projectPath('html_dir');
821
+ const htmlFiles = htmlDir ? [
822
+ { path: path.join(htmlDir, 'index.html'), name: 'index.html' },
823
+ { path: path.join(htmlDir, 'explorer.html'), name: 'explorer.html' },
824
+ { path: path.join(htmlDir, 'meet.html'), name: 'meet.html' }
825
+ ] : [];
755
826
  for (const hf of htmlFiles) {
756
827
  const content = safeReadFile(hf.path);
757
828
  if (!content) continue;
@@ -761,7 +832,7 @@ function auditRoles() {
761
832
  }
762
833
 
763
834
  // Check mcp-ops-server.js
764
- const opsContent = safeReadFile('/root/mcp-ops-server.js');
835
+ const opsContent = projectPath('project_root') ? safeReadFile(path.join(projectPath('project_root') || process.cwd(), 'mcp-ops-server.js')) : null;
765
836
  if (opsContent) {
766
837
  for (const [id, src] of Object.entries(sourceRoles)) {
767
838
  if (opsContent.includes(`"${src.aka}"`) || opsContent.includes(`'${src.aka}'`)) {
@@ -776,7 +847,10 @@ function auditRoles() {
776
847
  function auditVersions() {
777
848
  const drifts = [];
778
849
  let cleanChecks = 0;
779
- const pkgFile = '/root/github-repos/mcp-nervous-system/package.json';
850
+ const pkgFile = projectPath('package_json');
851
+ if (!pkgFile) {
852
+ return { drifts: [], cleanChecks: 0, skipped: 'package_json not configured' };
853
+ }
780
854
  const pkg = safeReadJSON(pkgFile);
781
855
  const expectedVersion = pkg ? pkg.version : null;
782
856
  if (!expectedVersion) {
@@ -785,7 +859,8 @@ function auditVersions() {
785
859
  }
786
860
 
787
861
  // Check SERVER_INFO.version and FRAMEWORK.version in index.js
788
- const indexContent = safeReadFile('/root/github-repos/mcp-nervous-system/index.js');
862
+ const ghRepo = projectPath('github_repo');
863
+ const indexContent = ghRepo ? safeReadFile(path.join(ghRepo, 'index.js')) : null;
789
864
  if (indexContent) {
790
865
  const siMatch = indexContent.match(/SERVER_INFO\s*=\s*\{[^}]*version:\s*'([^']+)'/);
791
866
  if (siMatch) {
@@ -826,7 +901,7 @@ function auditVersions() {
826
901
  }
827
902
 
828
903
  // Check BUSINESS_BUILDER.md
829
- const bbContent = safeReadFile('/root/family-data/BUSINESS_BUILDER.md');
904
+ const bbContent = projectPath('data_dir') ? safeReadFile(path.join(projectPath('data_dir'), 'BUSINESS_BUILDER.md')) : null;
830
905
  if (bbContent) {
831
906
  const bbMatch = bbContent.match(/[Nn]ervous [Ss]ystem.*?v?(\d+\.\d+\.\d+)/);
832
907
  if (bbMatch && bbMatch[1] !== expectedVersion) {
@@ -835,7 +910,7 @@ function auditVersions() {
835
910
  }
836
911
 
837
912
  // Check gateway.html
838
- const gwContent = safeReadFile('/root/family-home/gateway.html');
913
+ const gwContent = projectPath('html_dir') ? safeReadFile(path.join(projectPath('html_dir'), 'gateway.html')) : null;
839
914
  if (gwContent) {
840
915
  const gwMatch = gwContent.match(/[Vv]ersion[:\s]*v?(\d+\.\d+\.\d+)/);
841
916
  if (gwMatch && gwMatch[1] !== expectedVersion) {
@@ -844,7 +919,7 @@ function auditVersions() {
844
919
  }
845
920
 
846
921
  // Check README.md
847
- const readmeContent = safeReadFile('/root/github-repos/mcp-nervous-system/README.md');
922
+ const readmeContent = ghRepo ? safeReadFile(path.join(ghRepo, 'README.md')) : null;
848
923
  if (readmeContent) {
849
924
  const rmMatch = readmeContent.match(/[Vv]ersion[:\s]*v?(\d+\.\d+\.\d+)/);
850
925
  if (rmMatch && rmMatch[1] !== expectedVersion) {
@@ -853,7 +928,7 @@ function auditVersions() {
853
928
  }
854
929
 
855
930
  // Check family-roles.json stats
856
- const roles = safeReadJSON('/root/family-data/family-roles.json');
931
+ const roles = projectPath('roles_file') ? safeReadJSON(projectPath('roles_file')) : null;
857
932
  if (roles && roles.stats) {
858
933
  if (roles.stats.ns_version && roles.stats.ns_version !== expectedVersion) {
859
934
  drifts.push({ type: 'version_mismatch', source: 'package.json', target: 'family-roles.json', field: 'stats.ns_version', expected: expectedVersion, found: roles.stats.ns_version });
@@ -874,10 +949,13 @@ function auditFiles() {
874
949
  let cleanChecks = 0;
875
950
 
876
951
  // Check UNTOUCHABLE_FILES.txt - verify each file exists
877
- const untouchableContent = safeReadFile('/root/family-data/UNTOUCHABLE_FILES.txt');
952
+ const untouchableFile = projectPath('protected_files_list');
953
+ const untouchableContent = untouchableFile ? safeReadFile(untouchableFile) : null;
878
954
  if (untouchableContent) {
879
955
  const lines = untouchableContent.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
880
- for (const filePath of lines) {
956
+ for (const rawLine of lines) {
957
+ const filePath = rawLine.split(/\s*[\(\#]/)[0].trim();
958
+ if (!filePath || !filePath.startsWith('/')) continue;
881
959
  if (fs.existsSync(filePath)) {
882
960
  cleanChecks++;
883
961
  } else {
@@ -887,10 +965,8 @@ function auditFiles() {
887
965
  }
888
966
 
889
967
  // Check LLM_STARTUP.md and BUSINESS_BUILDER.md for file references
890
- const docsToCheck = [
891
- { path: '/root/family-data/LLM_STARTUP.md', name: 'LLM_STARTUP.md' },
892
- { path: '/root/family-data/BUSINESS_BUILDER.md', name: 'BUSINESS_BUILDER.md' }
893
- ];
968
+ const docsToAudit = PROJECT.docs_to_audit || [];
969
+ const docsToCheck = docsToAudit.map(p => ({ path: p, name: path.basename(p) }));
894
970
 
895
971
  // Get PM2 running scripts
896
972
  let pm2Scripts = {};
@@ -906,7 +982,8 @@ function auditFiles() {
906
982
  const content = safeReadFile(doc.path);
907
983
  if (!content) continue;
908
984
  // Look for .js file references
909
- const jsRefs = content.match(/\/root\/[^\s)]+\.js/g) || [];
985
+ // Match .js files but exclude .json, .jsonl, .jsx
986
+ const jsRefs = (content.match(/\/[^\s)]+\.js\b/g) || []).filter(r => !r.match(/\.json[l]?$/));
910
987
  for (const ref of jsRefs) {
911
988
  if (fs.existsSync(ref)) {
912
989
  cleanChecks++;
@@ -928,7 +1005,8 @@ function auditFiles() {
928
1005
  }
929
1006
 
930
1007
  // Check system-config.json syntax_check_scripts
931
- const config = safeReadJSON('/root/family-data/system-config.json');
1008
+ const sysConfigFile = projectPath('config_file');
1009
+ const config = sysConfigFile ? safeReadJSON(sysConfigFile) : null;
932
1010
  if (config && config.syntax_check_scripts) {
933
1011
  for (const script of config.syntax_check_scripts) {
934
1012
  if (fs.existsSync(script)) {
@@ -955,10 +1033,10 @@ function auditProcesses() {
955
1033
  return { drifts, cleanChecks };
956
1034
  }
957
1035
 
958
- const config = safeReadJSON('/root/family-data/system-config.json');
1036
+ const procConfigFile = projectPath('config_file');
1037
+ const config = procConfigFile ? safeReadJSON(procConfigFile) : null;
959
1038
  if (!config || !config.processes) {
960
- // Try to compare against family-roles.json procs
961
- const roles = safeReadJSON('/root/family-data/family-roles.json');
1039
+ const roles = projectPath('roles_file') ? safeReadJSON(projectPath('roles_file')) : null;
962
1040
  if (roles && roles.members) {
963
1041
  const expectedProcs = [];
964
1042
  for (const m of roles.members) {
@@ -1007,18 +1085,19 @@ function auditWebsite() {
1007
1085
  let cleanChecks = 0;
1008
1086
 
1009
1087
  // Source of truth values
1010
- const pkgFile = '/root/github-repos/mcp-nervous-system/package.json';
1011
- const pkg = safeReadJSON(pkgFile);
1012
- const expectedVersion = pkg ? pkg.version : '1.5.0';
1088
+ const pkgFile = projectPath('package_json');
1089
+ const pkg = pkgFile ? safeReadJSON(pkgFile) : null;
1090
+ const expectedVersion = pkg ? pkg.version : SERVER_INFO.version;
1013
1091
  const actualToolCount = TOOLS.length;
1014
1092
  const actualResourceCount = RESOURCES.length;
1015
1093
 
1016
- const roles = safeReadJSON('/root/family-data/family-roles.json');
1094
+ const roles = projectPath('roles_file') ? safeReadJSON(projectPath('roles_file')) : null;
1017
1095
  const expectedMemberCount = roles && roles.stats ? roles.stats.member_count : 11;
1018
1096
  const expectedProcessCount = roles && roles.stats ? roles.stats.process_count : 28;
1019
1097
 
1020
1098
  // Count protected files (non-comment, non-blank lines starting with /)
1021
- const untouchableContent = safeReadFile('/root/family-data/UNTOUCHABLE_FILES.txt');
1099
+ const protListFile = projectPath('protected_files_list');
1100
+ const untouchableContent = protListFile ? safeReadFile(protListFile) : null;
1022
1101
  let protectedFileCount = 0;
1023
1102
  if (untouchableContent) {
1024
1103
  protectedFileCount = untouchableContent.split('\n').filter(l => l.trim() && !l.trim().startsWith('#') && l.trim().startsWith('/')).length;
@@ -1028,7 +1107,10 @@ function auditWebsite() {
1028
1107
  const roleNames = roles && roles.members ? roles.members.map(m => m.name) : [];
1029
1108
 
1030
1109
  // 1. Check all .html files in /root/family-home/
1031
- const familyHomeDir = '/root/family-home/';
1110
+ const familyHomeDir = projectPath('html_dir');
1111
+ if (!familyHomeDir) {
1112
+ return { drifts: [], cleanChecks: 0, skipped: 'html_dir not configured' };
1113
+ }
1032
1114
  let htmlFiles = [];
1033
1115
  try {
1034
1116
  htmlFiles = fs.readdirSync(familyHomeDir).filter(f => f.endsWith('.html')).map(f => familyHomeDir + f);
@@ -1091,7 +1173,8 @@ function auditWebsite() {
1091
1173
  }
1092
1174
 
1093
1175
  // 2. Check family-guide.json
1094
- const guide = safeReadJSON('/root/family-data/family-guide.json');
1176
+ const guideFile2 = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'family-guide.json') : null;
1177
+ const guide = guideFile2 ? safeReadJSON(guideFile2) : null;
1095
1178
  if (guide) {
1096
1179
  const guideStr = JSON.stringify(guide);
1097
1180
  // Check version refs
@@ -1117,7 +1200,7 @@ function auditWebsite() {
1117
1200
  } else { cleanChecks++; }
1118
1201
 
1119
1202
  // 3. Check mcp-stripe-checkout.js for version refs
1120
- const checkoutContent = safeReadFile('/root/mcp-stripe-checkout.js');
1203
+ const checkoutContent = projectPath('project_root') ? safeReadFile(path.join(projectPath('project_root') || process.cwd(), 'mcp-stripe-checkout.js')) : null;
1121
1204
  if (checkoutContent) {
1122
1205
  const checkoutVersions = checkoutContent.match(/v(\d+\.\d+\.\d+)/g) || [];
1123
1206
  for (const cv of checkoutVersions) {
@@ -1130,7 +1213,8 @@ function auditWebsite() {
1130
1213
  }
1131
1214
 
1132
1215
  // 4. Check system-config.json for version/tool counts
1133
- const sysConfig = safeReadJSON('/root/family-data/system-config.json');
1216
+ const sysConfigFile2 = projectPath('config_file');
1217
+ const sysConfig = sysConfigFile2 ? safeReadJSON(sysConfigFile2) : null;
1134
1218
  if (sysConfig) {
1135
1219
  const scStr = JSON.stringify(sysConfig);
1136
1220
  const scVersions = scStr.match(/v(\d+\.\d+\.\d+)/g) || [];
@@ -1144,7 +1228,7 @@ function auditWebsite() {
1144
1228
  }
1145
1229
 
1146
1230
  // 5. Check FREE_TOOLS in mcp-api-middleware.js match actual tool names
1147
- const middlewareContent = safeReadFile('/root/mcp-api-middleware.js');
1231
+ const middlewareContent = projectPath('project_root') ? safeReadFile(path.join(projectPath('project_root') || process.cwd(), 'mcp-api-middleware.js')) : null;
1148
1232
  if (middlewareContent) {
1149
1233
  const freeToolsMatch = middlewareContent.match(/'nervous-system':\s*\[([^\]]+)\]/);
1150
1234
  if (freeToolsMatch) {
@@ -1164,7 +1248,7 @@ function auditWebsite() {
1164
1248
  }
1165
1249
 
1166
1250
  // 6. Check sitemap.xml has all public pages
1167
- const sitemapContent = safeReadFile('/root/family-home/sitemap.xml');
1251
+ const sitemapContent = familyHomeDir ? safeReadFile(path.join(familyHomeDir, 'sitemap.xml')) : null;
1168
1252
  if (sitemapContent && htmlFiles.length > 0) {
1169
1253
  const publicPages = htmlFiles.filter(f => {
1170
1254
  const name = f.split('/').pop();
@@ -1226,13 +1310,21 @@ function runSecurityAudit() {
1226
1310
  let checksPassed = 0;
1227
1311
 
1228
1312
  // 1. Scan HTML files for hardcoded passwords/secrets
1229
- const htmlDir = '/root/family-home/';
1313
+ const htmlDir = projectPath('html_dir');
1314
+ if (!htmlDir) {
1315
+ return { status: 'skipped', vulnerability_count: 0, checks_passed: 0, vulnerabilities: [], skipped: 'html_dir not configured' };
1316
+ }
1230
1317
  const secretPatterns = [
1231
- /\d{10}:AA[A-Za-z0-9_-]{30,}/g,
1232
- /sk-ant-[a-zA-Z0-9_-]+/g,
1233
- /npm_[A-Za-z0-9]{20,}/g,
1234
- /ghp_[A-Za-z0-9]{20,}/g,
1235
- /BOT_TOKEN\s*[:=]\s*['"][^'"]+['"]/gi,
1318
+ /\d{10}:AA[A-Za-z0-9_-]{30,}/g, // Telegram bot tokens
1319
+ /sk-ant-[a-zA-Z0-9_-]+/g, // Anthropic API keys
1320
+ /sk_live_[a-zA-Z0-9]+/g, // Stripe live keys
1321
+ /sk_test_[a-zA-Z0-9]+/g, // Stripe test keys
1322
+ /npm_[A-Za-z0-9]{20,}/g, // npm tokens
1323
+ /ghp_[A-Za-z0-9]{20,}/g, // GitHub PATs
1324
+ /BOT_TOKEN\s*[:=]\s*['"][^'"]+['"]/gi, // Generic bot tokens
1325
+ /password\s*[:=]\s*['"][^'"]{8,}['"]/gi, // Hardcoded passwords
1326
+ /api[_-]?key\s*[:=]\s*['"][A-Za-z0-9_\-]{20,}['"]/gi, // API keys
1327
+ /secret\s*[:=]\s*['"][A-Za-z0-9_\-]{16,}['"]/gi, // Secrets
1236
1328
  ];
1237
1329
  try {
1238
1330
  const htmlFiles = fs.readdirSync(htmlDir).filter(f => f.endsWith('.html'));
@@ -1240,11 +1332,24 @@ function runSecurityAudit() {
1240
1332
  const content = safeReadFile(htmlDir + hf);
1241
1333
  if (!content) continue;
1242
1334
  let fileClean = true;
1335
+ const contentLines = content.split('\n');
1243
1336
  for (const pat of secretPatterns) {
1244
1337
  pat.lastIndex = 0;
1245
- const matches = content.match(pat);
1246
- if (matches && matches.length > 0) {
1247
- vulnerabilities.push({ type: 'hardcoded_secret', file: hf, pattern: pat.source, count: matches.length });
1338
+ let realMatches = 0;
1339
+ for (const line of contentLines) {
1340
+ // Skip lines that are defining detection patterns (not actual secrets)
1341
+ if (line.trim().match(/^\s*\/.*\/[gim]*,?\s*$/) ||
1342
+ line.includes('SENS_PAT') ||
1343
+ line.includes('secretPatterns') ||
1344
+ line.includes('leakPatterns') ||
1345
+ line.includes('dangerPatterns') ||
1346
+ line.includes('redact')) continue;
1347
+ pat.lastIndex = 0;
1348
+ const m = line.match(pat);
1349
+ if (m) realMatches += m.length;
1350
+ }
1351
+ if (realMatches > 0) {
1352
+ vulnerabilities.push({ type: 'hardcoded_secret', file: hf, pattern: pat.source, count: realMatches });
1248
1353
  fileClean = false;
1249
1354
  }
1250
1355
  }
@@ -1255,7 +1360,7 @@ function runSecurityAudit() {
1255
1360
  }
1256
1361
 
1257
1362
  // 2. Check auth endpoints use server-side validation
1258
- const serverContent = safeReadFile('/root/family-home/server.js');
1363
+ const serverContent = htmlDir ? safeReadFile(path.join(htmlDir, 'server.js')) : null;
1259
1364
  if (serverContent) {
1260
1365
  if (serverContent.includes('getSessionFromReq') || serverContent.includes('getAccessLevel')) {
1261
1366
  checksPassed++;
@@ -1287,7 +1392,8 @@ function runSecurityAudit() {
1287
1392
  }
1288
1393
 
1289
1394
  // 5. Check bridge rate limiting
1290
- if (serverContent && serverContent.includes('rate') || fs.existsSync('/root/rate-limit.js') || fs.existsSync('/root/bridge-ratelimit.js')) {
1395
+ const projRoot = projectPath('project_root') || process.cwd();
1396
+ if (serverContent && serverContent.includes('rate') || fs.existsSync(path.join(projRoot, 'rate-limit.js')) || fs.existsSync(path.join(projRoot, 'bridge-ratelimit.js'))) {
1291
1397
  checksPassed++;
1292
1398
  } else {
1293
1399
  vulnerabilities.push({ type: 'missing_rate_limit', file: 'bridge', detail: 'No rate limiting found for bridge' });
@@ -1311,7 +1417,8 @@ function runSecurityAudit() {
1311
1417
 
1312
1418
  // 7. Check api-credentials.json permissions
1313
1419
  try {
1314
- const credFile = '/root/family-data/api-credentials.json';
1420
+ const credFile = projectPath('data_dir') ? path.join(projectPath('data_dir'), 'api-credentials.json') : null;
1421
+ if (!credFile) { checksPassed++; }
1315
1422
  if (fs.existsSync(credFile)) {
1316
1423
  const stats = fs.statSync(credFile);
1317
1424
  const mode = (stats.mode & 0o777).toString(8);
@@ -1363,10 +1470,14 @@ function runSecurityAudit() {
1363
1470
 
1364
1471
  function runAutoPropagators() {
1365
1472
  const results = [];
1473
+ const workersDir = projectPath('project_root') ? path.join(projectPath('project_root') || process.cwd(), 'family-workers') : null;
1474
+ if (!workersDir || !fs.existsSync(workersDir)) {
1475
+ return { timestamp: new Date().toISOString(), propagators_run: 0, results: [], skipped: 'family-workers directory not found' };
1476
+ }
1366
1477
  const scripts = [
1367
- { name: 'role', path: '/root/family-workers/role-propagator.js' },
1368
- { name: 'version', path: '/root/family-workers/version-propagator.js' },
1369
- { name: 'content', path: '/root/family-workers/content-propagator.js' }
1478
+ { name: 'role', path: path.join(workersDir, 'role-propagator.js') },
1479
+ { name: 'version', path: path.join(workersDir, 'version-propagator.js') },
1480
+ { name: 'content', path: path.join(workersDir, 'content-propagator.js') }
1370
1481
  ];
1371
1482
  for (const script of scripts) {
1372
1483
  try {
@@ -1389,7 +1500,10 @@ function runAutoPropagators() {
1389
1500
  // ============================================================
1390
1501
 
1391
1502
  function runPageHealth(page) {
1392
- const FAMILY_HOME = '/root/family-home';
1503
+ const FAMILY_HOME = projectPath('html_dir');
1504
+ if (!FAMILY_HOME) {
1505
+ return { status: 'skipped', pages_checked: 0, issue_count: 0, issues: [], skipped: 'html_dir not configured' };
1506
+ }
1393
1507
  const issues = [];
1394
1508
 
1395
1509
  let htmlFiles;
@@ -1543,6 +1657,111 @@ function runPageHealth(page) {
1543
1657
  };
1544
1658
  }
1545
1659
 
1660
+
1661
+ // ============================================================
1662
+ // PRE-PUBLISH AUDIT ENGINE
1663
+ // ============================================================
1664
+
1665
+ function runPrePublishAudit(sourceFile) {
1666
+ const findings = [];
1667
+ const file = sourceFile || __filename;
1668
+ let content;
1669
+ try {
1670
+ content = fs.readFileSync(file, 'utf8');
1671
+ } catch (e) {
1672
+ return { status: 'error', error: 'Cannot read file: ' + e.message };
1673
+ }
1674
+ const lines = content.split('\n');
1675
+
1676
+ // 1. Check for hardcoded absolute paths (non-portable)
1677
+ lines.forEach((line, idx) => {
1678
+ if (line.trim().startsWith('//')) return;
1679
+ if (line.includes('description:') || line.includes('context:')) return;
1680
+ if (line.includes('description,') || line.includes("description'")) return;
1681
+
1682
+ if (line.match(/['"\`]\/root\//)) {
1683
+ findings.push({
1684
+ type: 'hardcoded_path',
1685
+ line: idx + 1,
1686
+ preview: line.trim().substring(0, 100),
1687
+ fix: 'Use projectPath() or configurable path'
1688
+ });
1689
+ }
1690
+ if (line.match(/['"\`]\/home\//)) {
1691
+ findings.push({
1692
+ type: 'hardcoded_path',
1693
+ line: idx + 1,
1694
+ preview: line.trim().substring(0, 100),
1695
+ fix: 'Use projectPath() or os.homedir()'
1696
+ });
1697
+ }
1698
+ });
1699
+
1700
+ // 2. Check for personal data that should not ship
1701
+ const personalPatterns = [
1702
+ { name: 'email_address', pat: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
1703
+ { name: 'phone_number', pat: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g },
1704
+ { name: 'ip_address', pat: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g },
1705
+ ];
1706
+ lines.forEach((line, idx) => {
1707
+ if (line.trim().startsWith('//') || line.includes('description')) return;
1708
+ if (line.includes('regex') || line.includes('pattern') || line.includes('pat:')) return;
1709
+ for (const pp of personalPatterns) {
1710
+ pp.pat.lastIndex = 0;
1711
+ if (pp.pat.test(line)) {
1712
+ findings.push({
1713
+ type: 'personal_data',
1714
+ subtype: pp.name,
1715
+ line: idx + 1,
1716
+ preview: line.trim().substring(0, 100),
1717
+ });
1718
+ }
1719
+ }
1720
+ });
1721
+
1722
+ // 3. Check for internal naming that should be generic
1723
+ const internalTerms = [
1724
+ 'family-data', 'family-home', 'family-logs', 'family-roles',
1725
+ 'family-guide', 'family-status', 'family-workers',
1726
+ 'PAPA_FULL', 'PAPA_READ', 'ARTHUR_CHAT_ID',
1727
+ ];
1728
+ lines.forEach((line, idx) => {
1729
+ if (line.trim().startsWith('//')) return;
1730
+ if (line.includes('description:') || line.includes('context:') || line.includes('tagline:')) return;
1731
+ if (line.includes('origin_story')) return;
1732
+ for (const term of internalTerms) {
1733
+ if (line.toLowerCase().includes(term.toLowerCase()) &&
1734
+ !line.includes('// ')) {
1735
+ findings.push({
1736
+ type: 'internal_naming',
1737
+ term: term,
1738
+ line: idx + 1,
1739
+ preview: line.trim().substring(0, 100),
1740
+ });
1741
+ }
1742
+ }
1743
+ });
1744
+
1745
+ // 4. GATE: Block publish if critical findings
1746
+ const critical = findings.filter(f =>
1747
+ f.type === 'personal_data' ||
1748
+ (f.type === 'hardcoded_path' && !f.preview.includes('description'))
1749
+ );
1750
+
1751
+ return {
1752
+ status: findings.length === 0 ? 'ready_to_publish' :
1753
+ critical.length > 0 ? 'BLOCKED_critical_findings' : 'warnings_only',
1754
+ total_findings: findings.length,
1755
+ critical_count: critical.length,
1756
+ findings: findings,
1757
+ recommendation: critical.length > 0 ?
1758
+ 'DO NOT PUBLISH. Fix critical findings first.' :
1759
+ findings.length > 0 ?
1760
+ 'Review warnings before publishing. None are blockers.' :
1761
+ 'Clean. Safe to publish.'
1762
+ };
1763
+ }
1764
+
1546
1765
  // ============================================================
1547
1766
  // Handle tool calls
1548
1767
  // ============================================================
@@ -1668,6 +1887,10 @@ function handleToolCall(name, args) {
1668
1887
  return runPageHealth(args.page || 'all');
1669
1888
  }
1670
1889
 
1890
+ case 'pre_publish_audit': {
1891
+ return runPrePublishAudit(args.source_file);
1892
+ }
1893
+
1671
1894
  default:
1672
1895
  return { error: 'Unknown tool' };
1673
1896
  }
@@ -1789,7 +2012,7 @@ const server = http.createServer((req, res) => {
1789
2012
  // Health check
1790
2013
  if (req.method === 'GET' && url.pathname === '/health') {
1791
2014
  res.writeHead(200, { 'Content-Type': 'application/json' });
1792
- res.end(JSON.stringify({ status: 'ok', service: 'nervous-system-mcp', version: '1.5.0', protocol: MCP_VERSION }));
2015
+ res.end(JSON.stringify({ status: 'ok', service: 'nervous-system-mcp', version: '1.6.0', protocol: MCP_VERSION }));
1793
2016
  return;
1794
2017
  }
1795
2018
 
@@ -1906,7 +2129,7 @@ const server = http.createServer((req, res) => {
1906
2129
  res.writeHead(200, { 'Content-Type': 'application/json' });
1907
2130
  res.end(JSON.stringify({
1908
2131
  name: 'The Nervous System MCP Server',
1909
- version: '1.5.1',
2132
+ version: '1.6.0',
1910
2133
  protocol: MCP_VERSION,
1911
2134
  description: 'LLM behavioral enforcement framework. 7 core rules, preflight checks, session handoffs, worklogs, violation logging, kill switch, hash-chained audit, and forced reflection cycles. Built by Arthur Palyan.',
1912
2135
  endpoints: {
@@ -1928,8 +2151,8 @@ const server = http.createServer((req, res) => {
1928
2151
  migrateExistingViolations();
1929
2152
 
1930
2153
  server.listen(PORT, '127.0.0.1', () => {
1931
- console.error(`[MCP Server] Nervous System v1.5.1 running on port ${PORT}`);
2154
+ console.error(`[MCP Server] Nervous System v1.6.0 running on port ${PORT}`);
1932
2155
  console.error(`[MCP Server] SSE: /sse | HTTP: /mcp | Health: /health | Kill: POST /kill | Audit: GET /audit/verify | Dispatches: GET /dispatches`);
1933
2156
  console.error(`[MCP Server] Protocol: ${MCP_VERSION}`);
1934
- console.error(`[MCP Server] Tools: ${TOOLS.length} (including kill switch, audit chain, dispatch, drift audit, page health)`);
2157
+ console.error(`[MCP Server] Tools: ${TOOLS.length} (including kill switch, audit chain, dispatch, drift audit, page health, pre-publish audit)`);
1935
2158
  });
@@ -0,0 +1,10 @@
1
+ {
2
+ "project_root": ".",
3
+ "data_dir": "./data",
4
+ "logs_dir": "./logs",
5
+ "html_dir": "./public",
6
+ "protected_files_list": "./PROTECTED_FILES.txt",
7
+ "config_file": "./system-config.json",
8
+ "pm2_managed": false,
9
+ "docs_to_audit": []
10
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-nervous-system",
3
- "version": "1.5.2",
4
- "description": "The Nervous System - LLM Behavioral Enforcement Framework. 7 mechanically enforced rules, 16 tools including kill switch, audit chain, dispatch, drift audit, security audit, page health, and session close. MCP server for Claude Desktop, Claude Code, and any MCP-compatible client.",
3
+ "version": "1.6.0",
4
+ "description": "The Nervous System - LLM Behavioral Enforcement Framework. 7 mechanically enforced rules, 17 tools including kill switch, audit chain, dispatch, drift audit, security audit, page health, pre-publish audit, and session close. MCP server for Claude Desktop, Claude Code, and any MCP-compatible client.",
5
5
  "main": "server.js",
6
6
  "bin": {
7
7
  "mcp-nervous-system": "./stdio.js"