@kernel.chat/kbot 3.41.0 → 3.42.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.
Files changed (57) hide show
  1. package/README.md +5 -5
  2. package/dist/agent-teams.d.ts +1 -1
  3. package/dist/agent-teams.d.ts.map +1 -1
  4. package/dist/agent-teams.js +36 -3
  5. package/dist/agent-teams.js.map +1 -1
  6. package/dist/agents/specialists.d.ts.map +1 -1
  7. package/dist/agents/specialists.js +20 -0
  8. package/dist/agents/specialists.js.map +1 -1
  9. package/dist/channels/kbot-channel.js +8 -31
  10. package/dist/channels/kbot-channel.js.map +1 -1
  11. package/dist/cli.js +8 -8
  12. package/dist/digest.js +1 -1
  13. package/dist/digest.js.map +1 -1
  14. package/dist/email-service.d.ts.map +1 -1
  15. package/dist/email-service.js +1 -2
  16. package/dist/email-service.js.map +1 -1
  17. package/dist/episodic-memory.d.ts.map +1 -1
  18. package/dist/episodic-memory.js +14 -0
  19. package/dist/episodic-memory.js.map +1 -1
  20. package/dist/learned-router.d.ts.map +1 -1
  21. package/dist/learned-router.js +29 -0
  22. package/dist/learned-router.js.map +1 -1
  23. package/dist/tools/email.d.ts.map +1 -1
  24. package/dist/tools/email.js +2 -3
  25. package/dist/tools/email.js.map +1 -1
  26. package/dist/tools/index.d.ts.map +1 -1
  27. package/dist/tools/index.js +7 -1
  28. package/dist/tools/index.js.map +1 -1
  29. package/dist/tools/lab-bio.d.ts +2 -0
  30. package/dist/tools/lab-bio.d.ts.map +1 -0
  31. package/dist/tools/lab-bio.js +1392 -0
  32. package/dist/tools/lab-bio.js.map +1 -0
  33. package/dist/tools/lab-chem.d.ts +2 -0
  34. package/dist/tools/lab-chem.d.ts.map +1 -0
  35. package/dist/tools/lab-chem.js +1257 -0
  36. package/dist/tools/lab-chem.js.map +1 -0
  37. package/dist/tools/lab-core.d.ts +2 -0
  38. package/dist/tools/lab-core.d.ts.map +1 -0
  39. package/dist/tools/lab-core.js +2452 -0
  40. package/dist/tools/lab-core.js.map +1 -0
  41. package/dist/tools/lab-data.d.ts +2 -0
  42. package/dist/tools/lab-data.d.ts.map +1 -0
  43. package/dist/tools/lab-data.js +2464 -0
  44. package/dist/tools/lab-data.js.map +1 -0
  45. package/dist/tools/lab-earth.d.ts +2 -0
  46. package/dist/tools/lab-earth.d.ts.map +1 -0
  47. package/dist/tools/lab-earth.js +1124 -0
  48. package/dist/tools/lab-earth.js.map +1 -0
  49. package/dist/tools/lab-math.d.ts +2 -0
  50. package/dist/tools/lab-math.d.ts.map +1 -0
  51. package/dist/tools/lab-math.js +3021 -0
  52. package/dist/tools/lab-math.js.map +1 -0
  53. package/dist/tools/lab-physics.d.ts +2 -0
  54. package/dist/tools/lab-physics.d.ts.map +1 -0
  55. package/dist/tools/lab-physics.js +2423 -0
  56. package/dist/tools/lab-physics.js.map +1 -0
  57. package/package.json +2 -3
@@ -0,0 +1,1257 @@
1
+ // kbot Chemistry & Materials Science Tools
2
+ // Real implementations using PubChem, NIST, Rhea, Materials Project, COD.
3
+ // Local computation for stoichiometry, element lookup, and thermodynamics fallback.
4
+ import { registerTool } from './index.js';
5
+ const PERIODIC_TABLE = [
6
+ { number: 1, symbol: 'H', name: 'Hydrogen', atomicMass: 1.008, category: 'nonmetal', electronConfiguration: '1s1', electronegativity: 2.20, meltingPoint: 14.01, boilingPoint: 20.28, density: 0.00008988, ionizationEnergy: 1312.0, discoveryYear: 1766, crystalStructure: 'hexagonal' },
7
+ { number: 2, symbol: 'He', name: 'Helium', atomicMass: 4.0026, category: 'noble gas', electronConfiguration: '1s2', electronegativity: null, meltingPoint: 0.95, boilingPoint: 4.22, density: 0.0001785, ionizationEnergy: 2372.3, discoveryYear: 1868, crystalStructure: 'hexagonal close-packed' },
8
+ { number: 3, symbol: 'Li', name: 'Lithium', atomicMass: 6.941, category: 'alkali metal', electronConfiguration: '[He] 2s1', electronegativity: 0.98, meltingPoint: 453.69, boilingPoint: 1615, density: 0.534, ionizationEnergy: 520.2, discoveryYear: 1817, crystalStructure: 'body-centered cubic' },
9
+ { number: 4, symbol: 'Be', name: 'Beryllium', atomicMass: 9.0122, category: 'alkaline earth metal', electronConfiguration: '[He] 2s2', electronegativity: 1.57, meltingPoint: 1560, boilingPoint: 2742, density: 1.85, ionizationEnergy: 899.5, discoveryYear: 1798, crystalStructure: 'hexagonal close-packed' },
10
+ { number: 5, symbol: 'B', name: 'Boron', atomicMass: 10.81, category: 'metalloid', electronConfiguration: '[He] 2s2 2p1', electronegativity: 2.04, meltingPoint: 2349, boilingPoint: 4200, density: 2.34, ionizationEnergy: 800.6, discoveryYear: 1808, crystalStructure: 'rhombohedral' },
11
+ { number: 6, symbol: 'C', name: 'Carbon', atomicMass: 12.011, category: 'nonmetal', electronConfiguration: '[He] 2s2 2p2', electronegativity: 2.55, meltingPoint: 3823, boilingPoint: 4098, density: 2.267, ionizationEnergy: 1086.5, discoveryYear: null, crystalStructure: 'hexagonal' },
12
+ { number: 7, symbol: 'N', name: 'Nitrogen', atomicMass: 14.007, category: 'nonmetal', electronConfiguration: '[He] 2s2 2p3', electronegativity: 3.04, meltingPoint: 63.15, boilingPoint: 77.36, density: 0.0012506, ionizationEnergy: 1402.3, discoveryYear: 1772, crystalStructure: 'hexagonal' },
13
+ { number: 8, symbol: 'O', name: 'Oxygen', atomicMass: 15.999, category: 'nonmetal', electronConfiguration: '[He] 2s2 2p4', electronegativity: 3.44, meltingPoint: 54.36, boilingPoint: 90.20, density: 0.001429, ionizationEnergy: 1313.9, discoveryYear: 1774, crystalStructure: 'cubic' },
14
+ { number: 9, symbol: 'F', name: 'Fluorine', atomicMass: 18.998, category: 'halogen', electronConfiguration: '[He] 2s2 2p5', electronegativity: 3.98, meltingPoint: 53.53, boilingPoint: 85.03, density: 0.001696, ionizationEnergy: 1681.0, discoveryYear: 1886, crystalStructure: 'monoclinic' },
15
+ { number: 10, symbol: 'Ne', name: 'Neon', atomicMass: 20.180, category: 'noble gas', electronConfiguration: '[He] 2s2 2p6', electronegativity: null, meltingPoint: 24.56, boilingPoint: 27.07, density: 0.0008999, ionizationEnergy: 2080.7, discoveryYear: 1898, crystalStructure: 'face-centered cubic' },
16
+ { number: 11, symbol: 'Na', name: 'Sodium', atomicMass: 22.990, category: 'alkali metal', electronConfiguration: '[Ne] 3s1', electronegativity: 0.93, meltingPoint: 370.87, boilingPoint: 1156, density: 0.971, ionizationEnergy: 495.8, discoveryYear: 1807, crystalStructure: 'body-centered cubic' },
17
+ { number: 12, symbol: 'Mg', name: 'Magnesium', atomicMass: 24.305, category: 'alkaline earth metal', electronConfiguration: '[Ne] 3s2', electronegativity: 1.31, meltingPoint: 923, boilingPoint: 1363, density: 1.738, ionizationEnergy: 737.7, discoveryYear: 1808, crystalStructure: 'hexagonal close-packed' },
18
+ { number: 13, symbol: 'Al', name: 'Aluminium', atomicMass: 26.982, category: 'post-transition metal', electronConfiguration: '[Ne] 3s2 3p1', electronegativity: 1.61, meltingPoint: 933.47, boilingPoint: 2792, density: 2.698, ionizationEnergy: 577.5, discoveryYear: 1825, crystalStructure: 'face-centered cubic' },
19
+ { number: 14, symbol: 'Si', name: 'Silicon', atomicMass: 28.086, category: 'metalloid', electronConfiguration: '[Ne] 3s2 3p2', electronegativity: 1.90, meltingPoint: 1687, boilingPoint: 3538, density: 2.3296, ionizationEnergy: 786.5, discoveryYear: 1824, crystalStructure: 'diamond cubic' },
20
+ { number: 15, symbol: 'P', name: 'Phosphorus', atomicMass: 30.974, category: 'nonmetal', electronConfiguration: '[Ne] 3s2 3p3', electronegativity: 2.19, meltingPoint: 317.30, boilingPoint: 553.65, density: 1.82, ionizationEnergy: 1011.8, discoveryYear: 1669, crystalStructure: 'orthorhombic' },
21
+ { number: 16, symbol: 'S', name: 'Sulfur', atomicMass: 32.06, category: 'nonmetal', electronConfiguration: '[Ne] 3s2 3p4', electronegativity: 2.58, meltingPoint: 388.36, boilingPoint: 717.87, density: 2.067, ionizationEnergy: 999.6, discoveryYear: null, crystalStructure: 'orthorhombic' },
22
+ { number: 17, symbol: 'Cl', name: 'Chlorine', atomicMass: 35.45, category: 'halogen', electronConfiguration: '[Ne] 3s2 3p5', electronegativity: 3.16, meltingPoint: 171.6, boilingPoint: 239.11, density: 0.003214, ionizationEnergy: 1251.2, discoveryYear: 1774, crystalStructure: 'orthorhombic' },
23
+ { number: 18, symbol: 'Ar', name: 'Argon', atomicMass: 39.948, category: 'noble gas', electronConfiguration: '[Ne] 3s2 3p6', electronegativity: null, meltingPoint: 83.80, boilingPoint: 87.30, density: 0.0017837, ionizationEnergy: 1520.6, discoveryYear: 1894, crystalStructure: 'face-centered cubic' },
24
+ { number: 19, symbol: 'K', name: 'Potassium', atomicMass: 39.098, category: 'alkali metal', electronConfiguration: '[Ar] 4s1', electronegativity: 0.82, meltingPoint: 336.53, boilingPoint: 1032, density: 0.862, ionizationEnergy: 418.8, discoveryYear: 1807, crystalStructure: 'body-centered cubic' },
25
+ { number: 20, symbol: 'Ca', name: 'Calcium', atomicMass: 40.078, category: 'alkaline earth metal', electronConfiguration: '[Ar] 4s2', electronegativity: 1.00, meltingPoint: 1115, boilingPoint: 1757, density: 1.54, ionizationEnergy: 589.8, discoveryYear: 1808, crystalStructure: 'face-centered cubic' },
26
+ { number: 21, symbol: 'Sc', name: 'Scandium', atomicMass: 44.956, category: 'transition metal', electronConfiguration: '[Ar] 3d1 4s2', electronegativity: 1.36, meltingPoint: 1814, boilingPoint: 3109, density: 2.989, ionizationEnergy: 633.1, discoveryYear: 1879, crystalStructure: 'hexagonal close-packed' },
27
+ { number: 22, symbol: 'Ti', name: 'Titanium', atomicMass: 47.867, category: 'transition metal', electronConfiguration: '[Ar] 3d2 4s2', electronegativity: 1.54, meltingPoint: 1941, boilingPoint: 3560, density: 4.54, ionizationEnergy: 658.8, discoveryYear: 1791, crystalStructure: 'hexagonal close-packed' },
28
+ { number: 23, symbol: 'V', name: 'Vanadium', atomicMass: 50.942, category: 'transition metal', electronConfiguration: '[Ar] 3d3 4s2', electronegativity: 1.63, meltingPoint: 2183, boilingPoint: 3680, density: 6.11, ionizationEnergy: 650.9, discoveryYear: 1801, crystalStructure: 'body-centered cubic' },
29
+ { number: 24, symbol: 'Cr', name: 'Chromium', atomicMass: 51.996, category: 'transition metal', electronConfiguration: '[Ar] 3d5 4s1', electronegativity: 1.66, meltingPoint: 2180, boilingPoint: 2944, density: 7.15, ionizationEnergy: 652.9, discoveryYear: 1797, crystalStructure: 'body-centered cubic' },
30
+ { number: 25, symbol: 'Mn', name: 'Manganese', atomicMass: 54.938, category: 'transition metal', electronConfiguration: '[Ar] 3d5 4s2', electronegativity: 1.55, meltingPoint: 1519, boilingPoint: 2334, density: 7.44, ionizationEnergy: 717.3, discoveryYear: 1774, crystalStructure: 'body-centered cubic' },
31
+ { number: 26, symbol: 'Fe', name: 'Iron', atomicMass: 55.845, category: 'transition metal', electronConfiguration: '[Ar] 3d6 4s2', electronegativity: 1.83, meltingPoint: 1811, boilingPoint: 3134, density: 7.874, ionizationEnergy: 762.5, discoveryYear: null, crystalStructure: 'body-centered cubic' },
32
+ { number: 27, symbol: 'Co', name: 'Cobalt', atomicMass: 58.933, category: 'transition metal', electronConfiguration: '[Ar] 3d7 4s2', electronegativity: 1.88, meltingPoint: 1768, boilingPoint: 3200, density: 8.86, ionizationEnergy: 760.4, discoveryYear: 1735, crystalStructure: 'hexagonal close-packed' },
33
+ { number: 28, symbol: 'Ni', name: 'Nickel', atomicMass: 58.693, category: 'transition metal', electronConfiguration: '[Ar] 3d8 4s2', electronegativity: 1.91, meltingPoint: 1728, boilingPoint: 3186, density: 8.912, ionizationEnergy: 737.1, discoveryYear: 1751, crystalStructure: 'face-centered cubic' },
34
+ { number: 29, symbol: 'Cu', name: 'Copper', atomicMass: 63.546, category: 'transition metal', electronConfiguration: '[Ar] 3d10 4s1', electronegativity: 1.90, meltingPoint: 1357.77, boilingPoint: 2835, density: 8.96, ionizationEnergy: 745.5, discoveryYear: null, crystalStructure: 'face-centered cubic' },
35
+ { number: 30, symbol: 'Zn', name: 'Zinc', atomicMass: 65.38, category: 'transition metal', electronConfiguration: '[Ar] 3d10 4s2', electronegativity: 1.65, meltingPoint: 692.68, boilingPoint: 1180, density: 7.134, ionizationEnergy: 906.4, discoveryYear: null, crystalStructure: 'hexagonal close-packed' },
36
+ { number: 31, symbol: 'Ga', name: 'Gallium', atomicMass: 69.723, category: 'post-transition metal', electronConfiguration: '[Ar] 3d10 4s2 4p1', electronegativity: 1.81, meltingPoint: 302.91, boilingPoint: 2477, density: 5.907, ionizationEnergy: 578.8, discoveryYear: 1875, crystalStructure: 'orthorhombic' },
37
+ { number: 32, symbol: 'Ge', name: 'Germanium', atomicMass: 72.630, category: 'metalloid', electronConfiguration: '[Ar] 3d10 4s2 4p2', electronegativity: 2.01, meltingPoint: 1211.40, boilingPoint: 3106, density: 5.323, ionizationEnergy: 762.2, discoveryYear: 1886, crystalStructure: 'diamond cubic' },
38
+ { number: 33, symbol: 'As', name: 'Arsenic', atomicMass: 74.922, category: 'metalloid', electronConfiguration: '[Ar] 3d10 4s2 4p3', electronegativity: 2.18, meltingPoint: 1090, boilingPoint: 887, density: 5.776, ionizationEnergy: 947.0, discoveryYear: null, crystalStructure: 'rhombohedral' },
39
+ { number: 34, symbol: 'Se', name: 'Selenium', atomicMass: 78.971, category: 'nonmetal', electronConfiguration: '[Ar] 3d10 4s2 4p4', electronegativity: 2.55, meltingPoint: 493.65, boilingPoint: 958, density: 4.809, ionizationEnergy: 941.0, discoveryYear: 1817, crystalStructure: 'hexagonal' },
40
+ { number: 35, symbol: 'Br', name: 'Bromine', atomicMass: 79.904, category: 'halogen', electronConfiguration: '[Ar] 3d10 4s2 4p5', electronegativity: 2.96, meltingPoint: 265.8, boilingPoint: 332.0, density: 3.122, ionizationEnergy: 1139.9, discoveryYear: 1826, crystalStructure: 'orthorhombic' },
41
+ { number: 36, symbol: 'Kr', name: 'Krypton', atomicMass: 83.798, category: 'noble gas', electronConfiguration: '[Ar] 3d10 4s2 4p6', electronegativity: 3.00, meltingPoint: 115.79, boilingPoint: 119.93, density: 0.003733, ionizationEnergy: 1350.8, discoveryYear: 1898, crystalStructure: 'face-centered cubic' },
42
+ { number: 37, symbol: 'Rb', name: 'Rubidium', atomicMass: 85.468, category: 'alkali metal', electronConfiguration: '[Kr] 5s1', electronegativity: 0.82, meltingPoint: 312.46, boilingPoint: 961, density: 1.532, ionizationEnergy: 403.0, discoveryYear: 1861, crystalStructure: 'body-centered cubic' },
43
+ { number: 38, symbol: 'Sr', name: 'Strontium', atomicMass: 87.62, category: 'alkaline earth metal', electronConfiguration: '[Kr] 5s2', electronegativity: 0.95, meltingPoint: 1050, boilingPoint: 1655, density: 2.64, ionizationEnergy: 549.5, discoveryYear: 1790, crystalStructure: 'face-centered cubic' },
44
+ { number: 39, symbol: 'Y', name: 'Yttrium', atomicMass: 88.906, category: 'transition metal', electronConfiguration: '[Kr] 4d1 5s2', electronegativity: 1.22, meltingPoint: 1799, boilingPoint: 3609, density: 4.469, ionizationEnergy: 600.0, discoveryYear: 1794, crystalStructure: 'hexagonal close-packed' },
45
+ { number: 40, symbol: 'Zr', name: 'Zirconium', atomicMass: 91.224, category: 'transition metal', electronConfiguration: '[Kr] 4d2 5s2', electronegativity: 1.33, meltingPoint: 2128, boilingPoint: 4682, density: 6.506, ionizationEnergy: 640.1, discoveryYear: 1789, crystalStructure: 'hexagonal close-packed' },
46
+ { number: 41, symbol: 'Nb', name: 'Niobium', atomicMass: 92.906, category: 'transition metal', electronConfiguration: '[Kr] 4d4 5s1', electronegativity: 1.6, meltingPoint: 2750, boilingPoint: 5017, density: 8.57, ionizationEnergy: 652.1, discoveryYear: 1801, crystalStructure: 'body-centered cubic' },
47
+ { number: 42, symbol: 'Mo', name: 'Molybdenum', atomicMass: 95.95, category: 'transition metal', electronConfiguration: '[Kr] 4d5 5s1', electronegativity: 2.16, meltingPoint: 2896, boilingPoint: 4912, density: 10.22, ionizationEnergy: 684.3, discoveryYear: 1781, crystalStructure: 'body-centered cubic' },
48
+ { number: 43, symbol: 'Tc', name: 'Technetium', atomicMass: 97, category: 'transition metal', electronConfiguration: '[Kr] 4d5 5s2', electronegativity: 1.9, meltingPoint: 2430, boilingPoint: 4538, density: 11.5, ionizationEnergy: 702.0, discoveryYear: 1937, crystalStructure: 'hexagonal close-packed' },
49
+ { number: 44, symbol: 'Ru', name: 'Ruthenium', atomicMass: 101.07, category: 'transition metal', electronConfiguration: '[Kr] 4d7 5s1', electronegativity: 2.2, meltingPoint: 2607, boilingPoint: 4423, density: 12.37, ionizationEnergy: 710.2, discoveryYear: 1844, crystalStructure: 'hexagonal close-packed' },
50
+ { number: 45, symbol: 'Rh', name: 'Rhodium', atomicMass: 102.91, category: 'transition metal', electronConfiguration: '[Kr] 4d8 5s1', electronegativity: 2.28, meltingPoint: 2237, boilingPoint: 3968, density: 12.41, ionizationEnergy: 719.7, discoveryYear: 1803, crystalStructure: 'face-centered cubic' },
51
+ { number: 46, symbol: 'Pd', name: 'Palladium', atomicMass: 106.42, category: 'transition metal', electronConfiguration: '[Kr] 4d10', electronegativity: 2.20, meltingPoint: 1828.05, boilingPoint: 3236, density: 12.02, ionizationEnergy: 804.4, discoveryYear: 1803, crystalStructure: 'face-centered cubic' },
52
+ { number: 47, symbol: 'Ag', name: 'Silver', atomicMass: 107.87, category: 'transition metal', electronConfiguration: '[Kr] 4d10 5s1', electronegativity: 1.93, meltingPoint: 1234.93, boilingPoint: 2435, density: 10.501, ionizationEnergy: 731.0, discoveryYear: null, crystalStructure: 'face-centered cubic' },
53
+ { number: 48, symbol: 'Cd', name: 'Cadmium', atomicMass: 112.41, category: 'transition metal', electronConfiguration: '[Kr] 4d10 5s2', electronegativity: 1.69, meltingPoint: 594.22, boilingPoint: 1040, density: 8.69, ionizationEnergy: 867.8, discoveryYear: 1817, crystalStructure: 'hexagonal close-packed' },
54
+ { number: 49, symbol: 'In', name: 'Indium', atomicMass: 114.82, category: 'post-transition metal', electronConfiguration: '[Kr] 4d10 5s2 5p1', electronegativity: 1.78, meltingPoint: 429.75, boilingPoint: 2345, density: 7.31, ionizationEnergy: 558.3, discoveryYear: 1863, crystalStructure: 'tetragonal' },
55
+ { number: 50, symbol: 'Sn', name: 'Tin', atomicMass: 118.71, category: 'post-transition metal', electronConfiguration: '[Kr] 4d10 5s2 5p2', electronegativity: 1.96, meltingPoint: 505.08, boilingPoint: 2875, density: 7.287, ionizationEnergy: 708.6, discoveryYear: null, crystalStructure: 'tetragonal' },
56
+ { number: 51, symbol: 'Sb', name: 'Antimony', atomicMass: 121.76, category: 'metalloid', electronConfiguration: '[Kr] 4d10 5s2 5p3', electronegativity: 2.05, meltingPoint: 903.78, boilingPoint: 1860, density: 6.685, ionizationEnergy: 834.0, discoveryYear: null, crystalStructure: 'rhombohedral' },
57
+ { number: 52, symbol: 'Te', name: 'Tellurium', atomicMass: 127.60, category: 'metalloid', electronConfiguration: '[Kr] 4d10 5s2 5p4', electronegativity: 2.1, meltingPoint: 722.66, boilingPoint: 1261, density: 6.232, ionizationEnergy: 869.3, discoveryYear: 1783, crystalStructure: 'hexagonal' },
58
+ { number: 53, symbol: 'I', name: 'Iodine', atomicMass: 126.90, category: 'halogen', electronConfiguration: '[Kr] 4d10 5s2 5p5', electronegativity: 2.66, meltingPoint: 386.85, boilingPoint: 457.4, density: 4.93, ionizationEnergy: 1008.4, discoveryYear: 1811, crystalStructure: 'orthorhombic' },
59
+ { number: 54, symbol: 'Xe', name: 'Xenon', atomicMass: 131.29, category: 'noble gas', electronConfiguration: '[Kr] 4d10 5s2 5p6', electronegativity: 2.60, meltingPoint: 161.4, boilingPoint: 165.03, density: 0.005887, ionizationEnergy: 1170.4, discoveryYear: 1898, crystalStructure: 'face-centered cubic' },
60
+ { number: 55, symbol: 'Cs', name: 'Caesium', atomicMass: 132.91, category: 'alkali metal', electronConfiguration: '[Xe] 6s1', electronegativity: 0.79, meltingPoint: 301.59, boilingPoint: 944, density: 1.873, ionizationEnergy: 375.7, discoveryYear: 1860, crystalStructure: 'body-centered cubic' },
61
+ { number: 56, symbol: 'Ba', name: 'Barium', atomicMass: 137.33, category: 'alkaline earth metal', electronConfiguration: '[Xe] 6s2', electronegativity: 0.89, meltingPoint: 1000, boilingPoint: 2170, density: 3.594, ionizationEnergy: 502.9, discoveryYear: 1808, crystalStructure: 'body-centered cubic' },
62
+ { number: 57, symbol: 'La', name: 'Lanthanum', atomicMass: 138.91, category: 'lanthanide', electronConfiguration: '[Xe] 5d1 6s2', electronegativity: 1.10, meltingPoint: 1193, boilingPoint: 3737, density: 6.145, ionizationEnergy: 538.1, discoveryYear: 1839, crystalStructure: 'hexagonal close-packed' },
63
+ { number: 58, symbol: 'Ce', name: 'Cerium', atomicMass: 140.12, category: 'lanthanide', electronConfiguration: '[Xe] 4f1 5d1 6s2', electronegativity: 1.12, meltingPoint: 1068, boilingPoint: 3716, density: 6.77, ionizationEnergy: 534.4, discoveryYear: 1803, crystalStructure: 'face-centered cubic' },
64
+ { number: 59, symbol: 'Pr', name: 'Praseodymium', atomicMass: 140.91, category: 'lanthanide', electronConfiguration: '[Xe] 4f3 6s2', electronegativity: 1.13, meltingPoint: 1208, boilingPoint: 3793, density: 6.773, ionizationEnergy: 527.0, discoveryYear: 1885, crystalStructure: 'hexagonal close-packed' },
65
+ { number: 60, symbol: 'Nd', name: 'Neodymium', atomicMass: 144.24, category: 'lanthanide', electronConfiguration: '[Xe] 4f4 6s2', electronegativity: 1.14, meltingPoint: 1297, boilingPoint: 3347, density: 7.007, ionizationEnergy: 533.1, discoveryYear: 1885, crystalStructure: 'hexagonal close-packed' },
66
+ { number: 61, symbol: 'Pm', name: 'Promethium', atomicMass: 145, category: 'lanthanide', electronConfiguration: '[Xe] 4f5 6s2', electronegativity: 1.13, meltingPoint: 1315, boilingPoint: 3273, density: 7.26, ionizationEnergy: 540.0, discoveryYear: 1945, crystalStructure: 'hexagonal close-packed' },
67
+ { number: 62, symbol: 'Sm', name: 'Samarium', atomicMass: 150.36, category: 'lanthanide', electronConfiguration: '[Xe] 4f6 6s2', electronegativity: 1.17, meltingPoint: 1345, boilingPoint: 2067, density: 7.52, ionizationEnergy: 544.5, discoveryYear: 1879, crystalStructure: 'rhombohedral' },
68
+ { number: 63, symbol: 'Eu', name: 'Europium', atomicMass: 151.96, category: 'lanthanide', electronConfiguration: '[Xe] 4f7 6s2', electronegativity: 1.2, meltingPoint: 1099, boilingPoint: 1802, density: 5.243, ionizationEnergy: 547.1, discoveryYear: 1901, crystalStructure: 'body-centered cubic' },
69
+ { number: 64, symbol: 'Gd', name: 'Gadolinium', atomicMass: 157.25, category: 'lanthanide', electronConfiguration: '[Xe] 4f7 5d1 6s2', electronegativity: 1.20, meltingPoint: 1585, boilingPoint: 3546, density: 7.895, ionizationEnergy: 593.4, discoveryYear: 1880, crystalStructure: 'hexagonal close-packed' },
70
+ { number: 65, symbol: 'Tb', name: 'Terbium', atomicMass: 158.93, category: 'lanthanide', electronConfiguration: '[Xe] 4f9 6s2', electronegativity: 1.2, meltingPoint: 1629, boilingPoint: 3503, density: 8.229, ionizationEnergy: 565.8, discoveryYear: 1843, crystalStructure: 'hexagonal close-packed' },
71
+ { number: 66, symbol: 'Dy', name: 'Dysprosium', atomicMass: 162.50, category: 'lanthanide', electronConfiguration: '[Xe] 4f10 6s2', electronegativity: 1.22, meltingPoint: 1680, boilingPoint: 2840, density: 8.55, ionizationEnergy: 573.0, discoveryYear: 1886, crystalStructure: 'hexagonal close-packed' },
72
+ { number: 67, symbol: 'Ho', name: 'Holmium', atomicMass: 164.93, category: 'lanthanide', electronConfiguration: '[Xe] 4f11 6s2', electronegativity: 1.23, meltingPoint: 1734, boilingPoint: 2993, density: 8.795, ionizationEnergy: 581.0, discoveryYear: 1878, crystalStructure: 'hexagonal close-packed' },
73
+ { number: 68, symbol: 'Er', name: 'Erbium', atomicMass: 167.26, category: 'lanthanide', electronConfiguration: '[Xe] 4f12 6s2', electronegativity: 1.24, meltingPoint: 1802, boilingPoint: 3141, density: 9.066, ionizationEnergy: 589.3, discoveryYear: 1843, crystalStructure: 'hexagonal close-packed' },
74
+ { number: 69, symbol: 'Tm', name: 'Thulium', atomicMass: 168.93, category: 'lanthanide', electronConfiguration: '[Xe] 4f13 6s2', electronegativity: 1.25, meltingPoint: 1818, boilingPoint: 2223, density: 9.321, ionizationEnergy: 596.7, discoveryYear: 1879, crystalStructure: 'hexagonal close-packed' },
75
+ { number: 70, symbol: 'Yb', name: 'Ytterbium', atomicMass: 173.05, category: 'lanthanide', electronConfiguration: '[Xe] 4f14 6s2', electronegativity: 1.1, meltingPoint: 1097, boilingPoint: 1469, density: 6.965, ionizationEnergy: 603.4, discoveryYear: 1878, crystalStructure: 'face-centered cubic' },
76
+ { number: 71, symbol: 'Lu', name: 'Lutetium', atomicMass: 174.97, category: 'lanthanide', electronConfiguration: '[Xe] 4f14 5d1 6s2', electronegativity: 1.27, meltingPoint: 1925, boilingPoint: 3675, density: 9.84, ionizationEnergy: 523.5, discoveryYear: 1907, crystalStructure: 'hexagonal close-packed' },
77
+ { number: 72, symbol: 'Hf', name: 'Hafnium', atomicMass: 178.49, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d2 6s2', electronegativity: 1.3, meltingPoint: 2506, boilingPoint: 4876, density: 13.31, ionizationEnergy: 658.5, discoveryYear: 1923, crystalStructure: 'hexagonal close-packed' },
78
+ { number: 73, symbol: 'Ta', name: 'Tantalum', atomicMass: 180.95, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d3 6s2', electronegativity: 1.5, meltingPoint: 3290, boilingPoint: 5731, density: 16.654, ionizationEnergy: 761.0, discoveryYear: 1802, crystalStructure: 'body-centered cubic' },
79
+ { number: 74, symbol: 'W', name: 'Tungsten', atomicMass: 183.84, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d4 6s2', electronegativity: 2.36, meltingPoint: 3695, boilingPoint: 5828, density: 19.25, ionizationEnergy: 770.0, discoveryYear: 1783, crystalStructure: 'body-centered cubic' },
80
+ { number: 75, symbol: 'Re', name: 'Rhenium', atomicMass: 186.21, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d5 6s2', electronegativity: 1.9, meltingPoint: 3459, boilingPoint: 5869, density: 21.02, ionizationEnergy: 760.0, discoveryYear: 1925, crystalStructure: 'hexagonal close-packed' },
81
+ { number: 76, symbol: 'Os', name: 'Osmium', atomicMass: 190.23, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d6 6s2', electronegativity: 2.2, meltingPoint: 3306, boilingPoint: 5285, density: 22.587, ionizationEnergy: 840.0, discoveryYear: 1803, crystalStructure: 'hexagonal close-packed' },
82
+ { number: 77, symbol: 'Ir', name: 'Iridium', atomicMass: 192.22, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d7 6s2', electronegativity: 2.20, meltingPoint: 2739, boilingPoint: 4701, density: 22.56, ionizationEnergy: 880.0, discoveryYear: 1803, crystalStructure: 'face-centered cubic' },
83
+ { number: 78, symbol: 'Pt', name: 'Platinum', atomicMass: 195.08, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d9 6s1', electronegativity: 2.28, meltingPoint: 2041.4, boilingPoint: 4098, density: 21.46, ionizationEnergy: 870.0, discoveryYear: 1735, crystalStructure: 'face-centered cubic' },
84
+ { number: 79, symbol: 'Au', name: 'Gold', atomicMass: 196.97, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d10 6s1', electronegativity: 2.54, meltingPoint: 1337.33, boilingPoint: 3129, density: 19.282, ionizationEnergy: 890.1, discoveryYear: null, crystalStructure: 'face-centered cubic' },
85
+ { number: 80, symbol: 'Hg', name: 'Mercury', atomicMass: 200.59, category: 'transition metal', electronConfiguration: '[Xe] 4f14 5d10 6s2', electronegativity: 2.00, meltingPoint: 234.32, boilingPoint: 629.88, density: 13.5336, ionizationEnergy: 1007.1, discoveryYear: null, crystalStructure: 'rhombohedral' },
86
+ { number: 81, symbol: 'Tl', name: 'Thallium', atomicMass: 204.38, category: 'post-transition metal', electronConfiguration: '[Xe] 4f14 5d10 6s2 6p1', electronegativity: 1.62, meltingPoint: 577, boilingPoint: 1746, density: 11.85, ionizationEnergy: 589.4, discoveryYear: 1861, crystalStructure: 'hexagonal close-packed' },
87
+ { number: 82, symbol: 'Pb', name: 'Lead', atomicMass: 207.2, category: 'post-transition metal', electronConfiguration: '[Xe] 4f14 5d10 6s2 6p2', electronegativity: 1.87, meltingPoint: 600.61, boilingPoint: 2022, density: 11.342, ionizationEnergy: 715.6, discoveryYear: null, crystalStructure: 'face-centered cubic' },
88
+ { number: 83, symbol: 'Bi', name: 'Bismuth', atomicMass: 208.98, category: 'post-transition metal', electronConfiguration: '[Xe] 4f14 5d10 6s2 6p3', electronegativity: 2.02, meltingPoint: 544.55, boilingPoint: 1837, density: 9.807, ionizationEnergy: 703.0, discoveryYear: 1753, crystalStructure: 'rhombohedral' },
89
+ { number: 84, symbol: 'Po', name: 'Polonium', atomicMass: 209, category: 'metalloid', electronConfiguration: '[Xe] 4f14 5d10 6s2 6p4', electronegativity: 2.0, meltingPoint: 527, boilingPoint: 1235, density: 9.32, ionizationEnergy: 812.1, discoveryYear: 1898, crystalStructure: 'cubic' },
90
+ { number: 85, symbol: 'At', name: 'Astatine', atomicMass: 210, category: 'halogen', electronConfiguration: '[Xe] 4f14 5d10 6s2 6p5', electronegativity: 2.2, meltingPoint: 575, boilingPoint: 610, density: null, ionizationEnergy: 920.0, discoveryYear: 1940, crystalStructure: null },
91
+ { number: 86, symbol: 'Rn', name: 'Radon', atomicMass: 222, category: 'noble gas', electronConfiguration: '[Xe] 4f14 5d10 6s2 6p6', electronegativity: null, meltingPoint: 202, boilingPoint: 211.3, density: 0.00973, ionizationEnergy: 1037.0, discoveryYear: 1900, crystalStructure: 'face-centered cubic' },
92
+ { number: 87, symbol: 'Fr', name: 'Francium', atomicMass: 223, category: 'alkali metal', electronConfiguration: '[Rn] 7s1', electronegativity: 0.7, meltingPoint: 300, boilingPoint: 950, density: null, ionizationEnergy: 380.0, discoveryYear: 1939, crystalStructure: 'body-centered cubic' },
93
+ { number: 88, symbol: 'Ra', name: 'Radium', atomicMass: 226, category: 'alkaline earth metal', electronConfiguration: '[Rn] 7s2', electronegativity: 0.9, meltingPoint: 973, boilingPoint: 2010, density: 5.5, ionizationEnergy: 509.3, discoveryYear: 1898, crystalStructure: 'body-centered cubic' },
94
+ { number: 89, symbol: 'Ac', name: 'Actinium', atomicMass: 227, category: 'actinide', electronConfiguration: '[Rn] 6d1 7s2', electronegativity: 1.1, meltingPoint: 1323, boilingPoint: 3471, density: 10.07, ionizationEnergy: 499.0, discoveryYear: 1899, crystalStructure: 'face-centered cubic' },
95
+ { number: 90, symbol: 'Th', name: 'Thorium', atomicMass: 232.04, category: 'actinide', electronConfiguration: '[Rn] 6d2 7s2', electronegativity: 1.3, meltingPoint: 2023, boilingPoint: 5061, density: 11.72, ionizationEnergy: 587.0, discoveryYear: 1829, crystalStructure: 'face-centered cubic' },
96
+ { number: 91, symbol: 'Pa', name: 'Protactinium', atomicMass: 231.04, category: 'actinide', electronConfiguration: '[Rn] 5f2 6d1 7s2', electronegativity: 1.5, meltingPoint: 1841, boilingPoint: 4300, density: 15.37, ionizationEnergy: 568.0, discoveryYear: 1913, crystalStructure: 'tetragonal' },
97
+ { number: 92, symbol: 'U', name: 'Uranium', atomicMass: 238.03, category: 'actinide', electronConfiguration: '[Rn] 5f3 6d1 7s2', electronegativity: 1.38, meltingPoint: 1405.3, boilingPoint: 4404, density: 18.95, ionizationEnergy: 597.6, discoveryYear: 1789, crystalStructure: 'orthorhombic' },
98
+ { number: 93, symbol: 'Np', name: 'Neptunium', atomicMass: 237, category: 'actinide', electronConfiguration: '[Rn] 5f4 6d1 7s2', electronegativity: 1.36, meltingPoint: 917, boilingPoint: 4175, density: 20.45, ionizationEnergy: 604.5, discoveryYear: 1940, crystalStructure: 'orthorhombic' },
99
+ { number: 94, symbol: 'Pu', name: 'Plutonium', atomicMass: 244, category: 'actinide', electronConfiguration: '[Rn] 5f6 7s2', electronegativity: 1.28, meltingPoint: 912.5, boilingPoint: 3501, density: 19.84, ionizationEnergy: 584.7, discoveryYear: 1940, crystalStructure: 'monoclinic' },
100
+ { number: 95, symbol: 'Am', name: 'Americium', atomicMass: 243, category: 'actinide', electronConfiguration: '[Rn] 5f7 7s2', electronegativity: 1.3, meltingPoint: 1449, boilingPoint: 2880, density: 13.69, ionizationEnergy: 578.0, discoveryYear: 1944, crystalStructure: 'hexagonal close-packed' },
101
+ { number: 96, symbol: 'Cm', name: 'Curium', atomicMass: 247, category: 'actinide', electronConfiguration: '[Rn] 5f7 6d1 7s2', electronegativity: 1.3, meltingPoint: 1613, boilingPoint: 3383, density: 13.51, ionizationEnergy: 581.0, discoveryYear: 1944, crystalStructure: 'hexagonal close-packed' },
102
+ { number: 97, symbol: 'Bk', name: 'Berkelium', atomicMass: 247, category: 'actinide', electronConfiguration: '[Rn] 5f9 7s2', electronegativity: 1.3, meltingPoint: 1259, boilingPoint: 2900, density: 14.79, ionizationEnergy: 601.0, discoveryYear: 1949, crystalStructure: 'hexagonal close-packed' },
103
+ { number: 98, symbol: 'Cf', name: 'Californium', atomicMass: 251, category: 'actinide', electronConfiguration: '[Rn] 5f10 7s2', electronegativity: 1.3, meltingPoint: 1173, boilingPoint: 1743, density: 15.1, ionizationEnergy: 608.0, discoveryYear: 1950, crystalStructure: 'hexagonal close-packed' },
104
+ { number: 99, symbol: 'Es', name: 'Einsteinium', atomicMass: 252, category: 'actinide', electronConfiguration: '[Rn] 5f11 7s2', electronegativity: 1.3, meltingPoint: 1133, boilingPoint: 1269, density: 8.84, ionizationEnergy: 619.0, discoveryYear: 1952, crystalStructure: 'face-centered cubic' },
105
+ { number: 100, symbol: 'Fm', name: 'Fermium', atomicMass: 257, category: 'actinide', electronConfiguration: '[Rn] 5f12 7s2', electronegativity: 1.3, meltingPoint: 1800, boilingPoint: null, density: null, ionizationEnergy: 627.0, discoveryYear: 1952, crystalStructure: null },
106
+ { number: 101, symbol: 'Md', name: 'Mendelevium', atomicMass: 258, category: 'actinide', electronConfiguration: '[Rn] 5f13 7s2', electronegativity: 1.3, meltingPoint: 1100, boilingPoint: null, density: null, ionizationEnergy: 635.0, discoveryYear: 1955, crystalStructure: null },
107
+ { number: 102, symbol: 'No', name: 'Nobelium', atomicMass: 259, category: 'actinide', electronConfiguration: '[Rn] 5f14 7s2', electronegativity: 1.3, meltingPoint: 1100, boilingPoint: null, density: null, ionizationEnergy: 642.0, discoveryYear: 1958, crystalStructure: null },
108
+ { number: 103, symbol: 'Lr', name: 'Lawrencium', atomicMass: 266, category: 'actinide', electronConfiguration: '[Rn] 5f14 7s2 7p1', electronegativity: 1.3, meltingPoint: 1900, boilingPoint: null, density: null, ionizationEnergy: 470.0, discoveryYear: 1961, crystalStructure: null },
109
+ { number: 104, symbol: 'Rf', name: 'Rutherfordium', atomicMass: 267, category: 'transition metal', electronConfiguration: '[Rn] 5f14 6d2 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: 580.0, discoveryYear: 1964, crystalStructure: null },
110
+ { number: 105, symbol: 'Db', name: 'Dubnium', atomicMass: 268, category: 'transition metal', electronConfiguration: '[Rn] 5f14 6d3 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1967, crystalStructure: null },
111
+ { number: 106, symbol: 'Sg', name: 'Seaborgium', atomicMass: 269, category: 'transition metal', electronConfiguration: '[Rn] 5f14 6d4 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1974, crystalStructure: null },
112
+ { number: 107, symbol: 'Bh', name: 'Bohrium', atomicMass: 270, category: 'transition metal', electronConfiguration: '[Rn] 5f14 6d5 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1981, crystalStructure: null },
113
+ { number: 108, symbol: 'Hs', name: 'Hassium', atomicMass: 277, category: 'transition metal', electronConfiguration: '[Rn] 5f14 6d6 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1984, crystalStructure: null },
114
+ { number: 109, symbol: 'Mt', name: 'Meitnerium', atomicMass: 278, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d7 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1982, crystalStructure: null },
115
+ { number: 110, symbol: 'Ds', name: 'Darmstadtium', atomicMass: 281, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d8 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1994, crystalStructure: null },
116
+ { number: 111, symbol: 'Rg', name: 'Roentgenium', atomicMass: 282, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d9 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1994, crystalStructure: null },
117
+ { number: 112, symbol: 'Cn', name: 'Copernicium', atomicMass: 285, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d10 7s2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1996, crystalStructure: null },
118
+ { number: 113, symbol: 'Nh', name: 'Nihonium', atomicMass: 286, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d10 7s2 7p1', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 2003, crystalStructure: null },
119
+ { number: 114, symbol: 'Fl', name: 'Flerovium', atomicMass: 289, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d10 7s2 7p2', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 1998, crystalStructure: null },
120
+ { number: 115, symbol: 'Mc', name: 'Moscovium', atomicMass: 290, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d10 7s2 7p3', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 2003, crystalStructure: null },
121
+ { number: 116, symbol: 'Lv', name: 'Livermorium', atomicMass: 293, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d10 7s2 7p4', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 2000, crystalStructure: null },
122
+ { number: 117, symbol: 'Ts', name: 'Tennessine', atomicMass: 294, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d10 7s2 7p5', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 2010, crystalStructure: null },
123
+ { number: 118, symbol: 'Og', name: 'Oganesson', atomicMass: 294, category: 'unknown', electronConfiguration: '[Rn] 5f14 6d10 7s2 7p6', electronegativity: null, meltingPoint: null, boilingPoint: null, density: null, ionizationEnergy: null, discoveryYear: 2002, crystalStructure: null },
124
+ ];
125
+ // Index lookups for fast access
126
+ const bySymbol = new Map();
127
+ const byName = new Map();
128
+ const byNumber = new Map();
129
+ for (const el of PERIODIC_TABLE) {
130
+ bySymbol.set(el.symbol.toLowerCase(), el);
131
+ byName.set(el.name.toLowerCase(), el);
132
+ byNumber.set(el.number, el);
133
+ }
134
+ function lookupElement(query) {
135
+ const q = query.trim();
136
+ const num = Number(q);
137
+ if (!isNaN(num) && num >= 1 && num <= 118)
138
+ return byNumber.get(num);
139
+ const lower = q.toLowerCase();
140
+ return bySymbol.get(lower) || byName.get(lower);
141
+ }
142
+ const THERMO_TABLE = [
143
+ { formula: 'H2', name: 'hydrogen', deltaHf: 0, S: 130.68, Cp: 28.84, deltaGf: 0 },
144
+ { formula: 'O2', name: 'oxygen', deltaHf: 0, S: 205.14, Cp: 29.38, deltaGf: 0 },
145
+ { formula: 'N2', name: 'nitrogen', deltaHf: 0, S: 191.61, Cp: 29.12, deltaGf: 0 },
146
+ { formula: 'C', name: 'carbon (graphite)', deltaHf: 0, S: 5.74, Cp: 8.53, deltaGf: 0 },
147
+ { formula: 'C(diamond)', name: 'carbon (diamond)', deltaHf: 1.90, S: 2.38, Cp: 6.11, deltaGf: 2.90 },
148
+ { formula: 'S', name: 'sulfur (rhombic)', deltaHf: 0, S: 32.07, Cp: 22.64, deltaGf: 0 },
149
+ { formula: 'Fe', name: 'iron', deltaHf: 0, S: 27.28, Cp: 25.10, deltaGf: 0 },
150
+ { formula: 'Cu', name: 'copper', deltaHf: 0, S: 33.15, Cp: 24.44, deltaGf: 0 },
151
+ { formula: 'Al', name: 'aluminium', deltaHf: 0, S: 28.33, Cp: 24.35, deltaGf: 0 },
152
+ { formula: 'Na', name: 'sodium', deltaHf: 0, S: 51.21, Cp: 28.24, deltaGf: 0 },
153
+ { formula: 'K', name: 'potassium', deltaHf: 0, S: 64.18, Cp: 29.58, deltaGf: 0 },
154
+ { formula: 'Ca', name: 'calcium', deltaHf: 0, S: 41.42, Cp: 25.31, deltaGf: 0 },
155
+ { formula: 'Mg', name: 'magnesium', deltaHf: 0, S: 32.68, Cp: 24.89, deltaGf: 0 },
156
+ { formula: 'Zn', name: 'zinc', deltaHf: 0, S: 41.63, Cp: 25.40, deltaGf: 0 },
157
+ { formula: 'Ag', name: 'silver', deltaHf: 0, S: 42.55, Cp: 25.35, deltaGf: 0 },
158
+ { formula: 'H2O(l)', name: 'water (liquid)', deltaHf: -285.83, S: 69.91, Cp: 75.29, deltaGf: -237.13 },
159
+ { formula: 'H2O(g)', name: 'water (gas)', deltaHf: -241.82, S: 188.83, Cp: 33.58, deltaGf: -228.57 },
160
+ { formula: 'CO2', name: 'carbon dioxide', deltaHf: -393.51, S: 213.74, Cp: 37.11, deltaGf: -394.36 },
161
+ { formula: 'CO', name: 'carbon monoxide', deltaHf: -110.53, S: 197.67, Cp: 29.14, deltaGf: -137.17 },
162
+ { formula: 'CH4', name: 'methane', deltaHf: -74.81, S: 186.26, Cp: 35.31, deltaGf: -50.72 },
163
+ { formula: 'C2H6', name: 'ethane', deltaHf: -84.68, S: 229.60, Cp: 52.63, deltaGf: -32.82 },
164
+ { formula: 'C2H4', name: 'ethylene', deltaHf: 52.26, S: 219.56, Cp: 43.56, deltaGf: 68.15 },
165
+ { formula: 'C2H2', name: 'acetylene', deltaHf: 226.73, S: 200.94, Cp: 43.93, deltaGf: 209.20 },
166
+ { formula: 'C3H8', name: 'propane', deltaHf: -103.85, S: 269.91, Cp: 73.51, deltaGf: -23.49 },
167
+ { formula: 'C6H6(l)', name: 'benzene (liquid)', deltaHf: 49.03, S: 172.80, Cp: 136.1, deltaGf: 124.50 },
168
+ { formula: 'CH3OH(l)', name: 'methanol (liquid)', deltaHf: -238.66, S: 126.80, Cp: 81.6, deltaGf: -166.27 },
169
+ { formula: 'C2H5OH(l)', name: 'ethanol (liquid)', deltaHf: -277.69, S: 160.70, Cp: 111.46, deltaGf: -174.78 },
170
+ { formula: 'CH3COOH(l)', name: 'acetic acid (liquid)', deltaHf: -484.50, S: 159.80, Cp: 123.3, deltaGf: -389.90 },
171
+ { formula: 'NH3', name: 'ammonia', deltaHf: -46.11, S: 192.45, Cp: 35.06, deltaGf: -16.45 },
172
+ { formula: 'NO', name: 'nitric oxide', deltaHf: 90.25, S: 210.76, Cp: 29.84, deltaGf: 86.55 },
173
+ { formula: 'NO2', name: 'nitrogen dioxide', deltaHf: 33.18, S: 240.06, Cp: 37.20, deltaGf: 51.31 },
174
+ { formula: 'N2O', name: 'nitrous oxide', deltaHf: 82.05, S: 219.85, Cp: 38.45, deltaGf: 104.20 },
175
+ { formula: 'N2O4', name: 'dinitrogen tetroxide', deltaHf: 9.16, S: 304.29, Cp: 77.28, deltaGf: 97.89 },
176
+ { formula: 'HNO3(l)', name: 'nitric acid (liquid)', deltaHf: -174.10, S: 155.60, Cp: 109.87, deltaGf: -80.71 },
177
+ { formula: 'SO2', name: 'sulfur dioxide', deltaHf: -296.83, S: 248.22, Cp: 39.87, deltaGf: -300.19 },
178
+ { formula: 'SO3', name: 'sulfur trioxide', deltaHf: -395.72, S: 256.76, Cp: 50.67, deltaGf: -371.06 },
179
+ { formula: 'H2SO4(l)', name: 'sulfuric acid (liquid)', deltaHf: -813.99, S: 156.90, Cp: 138.9, deltaGf: -690.00 },
180
+ { formula: 'H2S', name: 'hydrogen sulfide', deltaHf: -20.63, S: 205.79, Cp: 34.23, deltaGf: -33.56 },
181
+ { formula: 'HCl', name: 'hydrogen chloride', deltaHf: -92.31, S: 186.91, Cp: 29.12, deltaGf: -95.30 },
182
+ { formula: 'HF', name: 'hydrogen fluoride', deltaHf: -271.10, S: 173.78, Cp: 29.13, deltaGf: -273.20 },
183
+ { formula: 'HBr', name: 'hydrogen bromide', deltaHf: -36.40, S: 198.70, Cp: 29.14, deltaGf: -53.45 },
184
+ { formula: 'HI', name: 'hydrogen iodide', deltaHf: 26.48, S: 206.59, Cp: 29.16, deltaGf: 1.70 },
185
+ { formula: 'NaCl', name: 'sodium chloride', deltaHf: -411.15, S: 72.13, Cp: 50.50, deltaGf: -384.14 },
186
+ { formula: 'NaOH', name: 'sodium hydroxide', deltaHf: -425.61, S: 64.46, Cp: 59.54, deltaGf: -379.49 },
187
+ { formula: 'KCl', name: 'potassium chloride', deltaHf: -436.75, S: 82.59, Cp: 51.30, deltaGf: -409.14 },
188
+ { formula: 'CaCO3', name: 'calcium carbonate', deltaHf: -1206.92, S: 92.88, Cp: 81.88, deltaGf: -1128.79 },
189
+ { formula: 'CaO', name: 'calcium oxide', deltaHf: -635.09, S: 39.75, Cp: 42.80, deltaGf: -604.03 },
190
+ { formula: 'Ca(OH)2', name: 'calcium hydroxide', deltaHf: -986.09, S: 83.39, Cp: 87.49, deltaGf: -898.49 },
191
+ { formula: 'MgO', name: 'magnesium oxide', deltaHf: -601.70, S: 26.94, Cp: 37.15, deltaGf: -569.43 },
192
+ { formula: 'Al2O3', name: 'aluminium oxide', deltaHf: -1675.70, S: 50.92, Cp: 79.04, deltaGf: -1582.30 },
193
+ { formula: 'Fe2O3', name: 'iron(III) oxide', deltaHf: -824.20, S: 87.40, Cp: 103.85, deltaGf: -742.20 },
194
+ { formula: 'Fe3O4', name: 'iron(II,III) oxide', deltaHf: -1118.40, S: 146.40, Cp: 143.43, deltaGf: -1015.40 },
195
+ { formula: 'FeO', name: 'iron(II) oxide', deltaHf: -272.00, S: 60.75, Cp: 49.92, deltaGf: -255.10 },
196
+ { formula: 'SiO2', name: 'silicon dioxide (quartz)', deltaHf: -910.94, S: 41.84, Cp: 44.43, deltaGf: -856.64 },
197
+ { formula: 'TiO2', name: 'titanium dioxide (rutile)', deltaHf: -944.70, S: 50.33, Cp: 55.02, deltaGf: -889.50 },
198
+ { formula: 'ZnO', name: 'zinc oxide', deltaHf: -348.28, S: 43.64, Cp: 40.25, deltaGf: -318.30 },
199
+ { formula: 'CuO', name: 'copper(II) oxide', deltaHf: -157.30, S: 42.63, Cp: 42.30, deltaGf: -129.70 },
200
+ { formula: 'Cu2O', name: 'copper(I) oxide', deltaHf: -168.60, S: 93.14, Cp: 63.64, deltaGf: -146.00 },
201
+ { formula: 'PbO', name: 'lead(II) oxide', deltaHf: -219.00, S: 66.50, Cp: 45.77, deltaGf: -188.93 },
202
+ { formula: 'AgCl', name: 'silver chloride', deltaHf: -127.07, S: 96.20, Cp: 50.79, deltaGf: -109.79 },
203
+ { formula: 'BaSO4', name: 'barium sulfate', deltaHf: -1473.20, S: 132.20, Cp: 101.75, deltaGf: -1362.20 },
204
+ { formula: 'KMnO4', name: 'potassium permanganate', deltaHf: -837.20, S: 171.71, Cp: 117.6, deltaGf: -737.60 },
205
+ { formula: 'KClO3', name: 'potassium chlorate', deltaHf: -397.73, S: 143.10, Cp: 100.25, deltaGf: -296.25 },
206
+ { formula: 'NaHCO3', name: 'sodium bicarbonate', deltaHf: -950.81, S: 101.70, Cp: 87.61, deltaGf: -851.00 },
207
+ { formula: 'Na2CO3', name: 'sodium carbonate', deltaHf: -1130.68, S: 135.00, Cp: 112.30, deltaGf: -1044.44 },
208
+ { formula: 'H2O2(l)', name: 'hydrogen peroxide (liquid)', deltaHf: -187.78, S: 109.60, Cp: 89.1, deltaGf: -120.35 },
209
+ { formula: 'O3', name: 'ozone', deltaHf: 142.70, S: 238.93, Cp: 39.24, deltaGf: 163.20 },
210
+ { formula: 'Cl2', name: 'chlorine', deltaHf: 0, S: 223.07, Cp: 33.91, deltaGf: 0 },
211
+ { formula: 'Br2(l)', name: 'bromine (liquid)', deltaHf: 0, S: 152.23, Cp: 75.69, deltaGf: 0 },
212
+ { formula: 'I2', name: 'iodine', deltaHf: 0, S: 116.14, Cp: 54.44, deltaGf: 0 },
213
+ { formula: 'F2', name: 'fluorine', deltaHf: 0, S: 202.78, Cp: 31.30, deltaGf: 0 },
214
+ { formula: 'P4', name: 'phosphorus (white)', deltaHf: 0, S: 164.36, Cp: 95.40, deltaGf: 0 },
215
+ { formula: 'PCl3', name: 'phosphorus trichloride', deltaHf: -287.00, S: 311.78, Cp: 71.84, deltaGf: -267.80 },
216
+ { formula: 'PCl5', name: 'phosphorus pentachloride', deltaHf: -374.90, S: 364.58, Cp: 112.80, deltaGf: -305.00 },
217
+ { formula: 'POCl3', name: 'phosphoryl chloride', deltaHf: -597.10, S: 222.50, Cp: 84.90, deltaGf: -520.80 },
218
+ { formula: 'CCl4(l)', name: 'carbon tetrachloride (liquid)', deltaHf: -135.44, S: 216.40, Cp: 131.75, deltaGf: -65.21 },
219
+ { formula: 'CHCl3(l)', name: 'chloroform (liquid)', deltaHf: -134.47, S: 201.70, Cp: 114.2, deltaGf: -73.66 },
220
+ { formula: 'CS2(l)', name: 'carbon disulfide (liquid)', deltaHf: 89.70, S: 151.34, Cp: 75.7, deltaGf: 65.27 },
221
+ { formula: 'COCl2', name: 'phosgene', deltaHf: -218.80, S: 283.53, Cp: 57.66, deltaGf: -204.60 },
222
+ { formula: 'glucose', name: 'glucose (C6H12O6)', deltaHf: -1274.40, S: 212.10, Cp: 218.0, deltaGf: -910.30 },
223
+ { formula: 'sucrose', name: 'sucrose (C12H22O11)', deltaHf: -2222.10, S: 360.20, Cp: 424.3, deltaGf: -1544.65 },
224
+ { formula: 'urea', name: 'urea (CH4N2O)', deltaHf: -333.50, S: 104.60, Cp: 93.14, deltaGf: -197.33 },
225
+ { formula: 'C(g)', name: 'carbon (gas)', deltaHf: 716.68, S: 158.10, Cp: 20.84, deltaGf: 671.26 },
226
+ { formula: 'H(g)', name: 'hydrogen atom (gas)', deltaHf: 217.97, S: 114.71, Cp: 20.78, deltaGf: 203.25 },
227
+ { formula: 'O(g)', name: 'oxygen atom (gas)', deltaHf: 249.17, S: 161.06, Cp: 21.91, deltaGf: 231.73 },
228
+ { formula: 'N(g)', name: 'nitrogen atom (gas)', deltaHf: 472.70, S: 153.30, Cp: 20.79, deltaGf: 455.56 },
229
+ { formula: 'Cl(g)', name: 'chlorine atom (gas)', deltaHf: 121.68, S: 165.20, Cp: 21.84, deltaGf: 105.68 },
230
+ { formula: 'Na+(aq)', name: 'sodium ion (aqueous)', deltaHf: -240.12, S: 59.00, Cp: 46.4, deltaGf: -261.91 },
231
+ { formula: 'Cl-(aq)', name: 'chloride ion (aqueous)', deltaHf: -167.16, S: 56.50, Cp: -136.4, deltaGf: -131.23 },
232
+ { formula: 'H+(aq)', name: 'hydrogen ion (aqueous)', deltaHf: 0, S: 0, Cp: 0, deltaGf: 0 },
233
+ { formula: 'OH-(aq)', name: 'hydroxide ion (aqueous)', deltaHf: -229.99, S: -10.75, Cp: -148.5, deltaGf: -157.24 },
234
+ { formula: 'Ca2+(aq)', name: 'calcium ion (aqueous)', deltaHf: -542.83, S: -53.10, Cp: null, deltaGf: -553.58 },
235
+ { formula: 'Fe2+(aq)', name: 'iron(II) ion (aqueous)', deltaHf: -89.10, S: -137.70, Cp: null, deltaGf: -78.90 },
236
+ { formula: 'Fe3+(aq)', name: 'iron(III) ion (aqueous)', deltaHf: -48.50, S: -315.90, Cp: null, deltaGf: -4.70 },
237
+ { formula: 'Cu2+(aq)', name: 'copper(II) ion (aqueous)', deltaHf: 64.77, S: -99.60, Cp: null, deltaGf: 65.49 },
238
+ { formula: 'Zn2+(aq)', name: 'zinc ion (aqueous)', deltaHf: -153.89, S: -112.10, Cp: null, deltaGf: -147.06 },
239
+ { formula: 'Ag+(aq)', name: 'silver ion (aqueous)', deltaHf: 105.58, S: 72.68, Cp: null, deltaGf: 77.11 },
240
+ { formula: 'CO3^2-(aq)', name: 'carbonate ion (aqueous)', deltaHf: -677.14, S: -56.90, Cp: null, deltaGf: -527.81 },
241
+ { formula: 'SO4^2-(aq)', name: 'sulfate ion (aqueous)', deltaHf: -909.27, S: 20.10, Cp: null, deltaGf: -744.53 },
242
+ { formula: 'NO3-(aq)', name: 'nitrate ion (aqueous)', deltaHf: -205.00, S: 146.40, Cp: null, deltaGf: -108.74 },
243
+ { formula: 'PO4^3-(aq)', name: 'phosphate ion (aqueous)', deltaHf: -1277.40, S: -220.50, Cp: null, deltaGf: -1018.70 },
244
+ ];
245
+ const thermoByFormula = new Map();
246
+ const thermoByName = new Map();
247
+ for (const t of THERMO_TABLE) {
248
+ thermoByFormula.set(t.formula.toLowerCase(), t);
249
+ thermoByName.set(t.name.toLowerCase(), t);
250
+ }
251
+ function lookupThermo(query) {
252
+ const q = query.trim().toLowerCase();
253
+ return thermoByFormula.get(q) || thermoByName.get(q) ||
254
+ THERMO_TABLE.find(t => t.name.toLowerCase().includes(q) || t.formula.toLowerCase().includes(q));
255
+ }
256
+ // ─────────────────────────────────────────────────────────────────────────────
257
+ // Formula parser & stoichiometry engine
258
+ // ─────────────────────────────────────────────────────────────────────────────
259
+ /** Parse a chemical formula into element counts: "Ca(OH)2" → {Ca:1, O:2, H:2} */
260
+ function parseFormula(formula) {
261
+ const counts = {};
262
+ const stack = [counts];
263
+ let i = 0;
264
+ while (i < formula.length) {
265
+ const ch = formula[i];
266
+ if (ch === '(') {
267
+ const inner = {};
268
+ stack.push(inner);
269
+ i++;
270
+ }
271
+ else if (ch === ')') {
272
+ i++;
273
+ // Read subscript after closing paren
274
+ let numStr = '';
275
+ while (i < formula.length && /\d/.test(formula[i])) {
276
+ numStr += formula[i];
277
+ i++;
278
+ }
279
+ const mult = numStr ? parseInt(numStr, 10) : 1;
280
+ const inner = stack.pop();
281
+ const outer = stack[stack.length - 1];
282
+ for (const [el, cnt] of Object.entries(inner)) {
283
+ outer[el] = (outer[el] || 0) + cnt * mult;
284
+ }
285
+ }
286
+ else if (ch === '·' || ch === '.') {
287
+ // Hydrate: e.g. CuSO4·5H2O
288
+ i++;
289
+ let numStr = '';
290
+ while (i < formula.length && /\d/.test(formula[i])) {
291
+ numStr += formula[i];
292
+ i++;
293
+ }
294
+ const mult = numStr ? parseInt(numStr, 10) : 1;
295
+ const rest = formula.slice(i);
296
+ const hydrateCounts = parseFormula(rest);
297
+ const target = stack[stack.length - 1];
298
+ for (const [el, cnt] of Object.entries(hydrateCounts)) {
299
+ target[el] = (target[el] || 0) + cnt * mult;
300
+ }
301
+ i = formula.length; // consumed rest
302
+ }
303
+ else if (/[A-Z]/.test(ch)) {
304
+ // Element symbol: uppercase optionally followed by lowercase
305
+ let sym = ch;
306
+ i++;
307
+ while (i < formula.length && /[a-z]/.test(formula[i])) {
308
+ sym += formula[i];
309
+ i++;
310
+ }
311
+ // Read subscript
312
+ let numStr = '';
313
+ while (i < formula.length && /\d/.test(formula[i])) {
314
+ numStr += formula[i];
315
+ i++;
316
+ }
317
+ const cnt = numStr ? parseInt(numStr, 10) : 1;
318
+ const target = stack[stack.length - 1];
319
+ target[sym] = (target[sym] || 0) + cnt;
320
+ }
321
+ else {
322
+ i++; // skip unknown chars
323
+ }
324
+ }
325
+ return counts;
326
+ }
327
+ /** Get the molecular weight of a formula */
328
+ function formulaWeight(formula) {
329
+ const counts = parseFormula(formula);
330
+ let mw = 0;
331
+ for (const [sym, cnt] of Object.entries(counts)) {
332
+ const el = bySymbol.get(sym.toLowerCase());
333
+ if (el)
334
+ mw += el.atomicMass * cnt;
335
+ }
336
+ return mw;
337
+ }
338
+ /** Format element counts as formula string */
339
+ function countsToFormula(counts) {
340
+ return Object.entries(counts)
341
+ .map(([el, n]) => n === 1 ? el : `${el}${n}`)
342
+ .join('');
343
+ }
344
+ /**
345
+ * Balance a chemical equation using Gaussian elimination.
346
+ * Input: "Fe + O2 -> Fe2O3"
347
+ * Output: [4, 3, 2] meaning 4Fe + 3O2 -> 2Fe2O3
348
+ */
349
+ function balanceEquation(equation) {
350
+ // Parse equation
351
+ const sides = equation.split(/->|→|=/);
352
+ if (sides.length !== 2)
353
+ return 'Error: equation must have exactly one arrow (->)';
354
+ const reactants = sides[0].split('+').map(s => s.trim()).filter(Boolean);
355
+ const products = sides[1].split('+').map(s => s.trim()).filter(Boolean);
356
+ const compounds = [...reactants, ...products];
357
+ const n = compounds.length;
358
+ if (n < 2)
359
+ return 'Error: need at least 2 compounds';
360
+ // Parse each compound and collect all elements
361
+ const parsed = compounds.map(c => parseFormula(c));
362
+ const elements = new Set();
363
+ for (const p of parsed) {
364
+ for (const el of Object.keys(p))
365
+ elements.add(el);
366
+ }
367
+ const elList = Array.from(elements);
368
+ const m = elList.length; // rows = elements
369
+ // Build matrix: rows = elements, cols = compounds
370
+ // Reactants have positive coefficients, products negative
371
+ const matrix = [];
372
+ for (let r = 0; r < m; r++) {
373
+ const row = [];
374
+ for (let c = 0; c < n; c++) {
375
+ const sign = c < reactants.length ? 1 : -1;
376
+ row.push(sign * (parsed[c][elList[r]] || 0));
377
+ }
378
+ matrix.push(row);
379
+ }
380
+ // Gaussian elimination on augmented-like system (homogeneous: Ax = 0)
381
+ // We solve for x[0..n-2] in terms of x[n-1] (set x[n-1] = free variable)
382
+ const rows = matrix.length;
383
+ const cols = n;
384
+ // Forward elimination
385
+ let pivotRow = 0;
386
+ const pivotCols = [];
387
+ for (let col = 0; col < cols - 1 && pivotRow < rows; col++) {
388
+ // Find best pivot
389
+ let maxVal = 0;
390
+ let maxRow = -1;
391
+ for (let r = pivotRow; r < rows; r++) {
392
+ if (Math.abs(matrix[r][col]) > maxVal) {
393
+ maxVal = Math.abs(matrix[r][col]);
394
+ maxRow = r;
395
+ }
396
+ }
397
+ if (maxVal < 1e-10)
398
+ continue // skip this column
399
+ ;
400
+ [matrix[pivotRow], matrix[maxRow]] = [matrix[maxRow], matrix[pivotRow]];
401
+ pivotCols.push(col);
402
+ // Eliminate below
403
+ for (let r = 0; r < rows; r++) {
404
+ if (r === pivotRow)
405
+ continue;
406
+ const factor = matrix[r][col] / matrix[pivotRow][col];
407
+ for (let c = col; c < cols; c++) {
408
+ matrix[r][c] -= factor * matrix[pivotRow][c];
409
+ }
410
+ }
411
+ pivotRow++;
412
+ }
413
+ // Back-substitute: set last variable = 1, solve rest
414
+ const solution = new Array(n).fill(0);
415
+ solution[n - 1] = 1;
416
+ // Work backwards through pivot columns
417
+ for (let i = pivotCols.length - 1; i >= 0; i--) {
418
+ const col = pivotCols[i];
419
+ const row = i;
420
+ let sum = 0;
421
+ for (let c = col + 1; c < cols; c++) {
422
+ sum += matrix[row][c] * solution[c];
423
+ }
424
+ if (Math.abs(matrix[row][col]) < 1e-10)
425
+ continue;
426
+ solution[col] = -sum / matrix[row][col];
427
+ }
428
+ // Check for zero solutions
429
+ if (solution.every(v => Math.abs(v) < 1e-10)) {
430
+ return 'Error: could not balance equation (trivial solution)';
431
+ }
432
+ // Make all positive (flip if needed)
433
+ const hasNeg = solution.some(v => v < -1e-10);
434
+ if (hasNeg) {
435
+ for (let i = 0; i < n; i++)
436
+ solution[i] = -solution[i];
437
+ }
438
+ // If any are still negative, the equation may be impossible
439
+ if (solution.some(v => v < -1e-10)) {
440
+ return 'Error: could not find non-negative integer solution';
441
+ }
442
+ // Convert to smallest integers: find LCM of denominators
443
+ // First, find a common scale by rounding to rational numbers
444
+ const minPositive = Math.min(...solution.filter(v => v > 1e-10));
445
+ const normalized = solution.map(v => v / minPositive);
446
+ // Try multipliers 1..1000 to find integer solution
447
+ for (let mult = 1; mult <= 1000; mult++) {
448
+ const scaled = normalized.map(v => Math.round(v * mult));
449
+ // Verify: check if scaled values reproduce the original ratios
450
+ const valid = scaled.every(v => v > 0) &&
451
+ normalized.every((v, i) => Math.abs(scaled[i] / mult - v) < 0.01);
452
+ if (valid) {
453
+ // Find GCD of all coefficients
454
+ let g = scaled[0];
455
+ for (let i = 1; i < scaled.length; i++)
456
+ g = gcd(g, scaled[i]);
457
+ const final = scaled.map(v => v / g);
458
+ return { reactants, products, coefficients: final };
459
+ }
460
+ }
461
+ // Fallback: round to nearest integer
462
+ const rounded = normalized.map(v => Math.max(1, Math.round(v)));
463
+ return { reactants, products, coefficients: rounded };
464
+ }
465
+ function gcd(a, b) {
466
+ a = Math.abs(a);
467
+ b = Math.abs(b);
468
+ while (b) {
469
+ [a, b] = [b, a % b];
470
+ }
471
+ return a;
472
+ }
473
+ // ─────────────────────────────────────────────────────────────────────────────
474
+ // Shared fetch helper
475
+ // ─────────────────────────────────────────────────────────────────────────────
476
+ const UA = 'KBot/3.0 (Lab Tools)';
477
+ async function labFetch(url) {
478
+ return fetch(url, {
479
+ headers: { 'User-Agent': UA, Accept: 'application/json,text/html,*/*' },
480
+ signal: AbortSignal.timeout(10000),
481
+ });
482
+ }
483
+ // ─────────────────────────────────────────────────────────────────────────────
484
+ // Tool registration
485
+ // ─────────────────────────────────────────────────────────────────────────────
486
+ export function registerLabChemTools() {
487
+ // ── 1. compound_search ──────────────────────────────────────────────────
488
+ registerTool({
489
+ name: 'compound_search',
490
+ description: 'Search PubChem for chemical compounds by name, molecular formula, SMILES, or InChI. Returns CID, IUPAC name, molecular formula, molecular weight, SMILES, and InChI.',
491
+ parameters: {
492
+ query: { type: 'string', description: 'Compound name, formula, SMILES, or InChI string', required: true },
493
+ search_type: { type: 'string', description: 'Search namespace: name, formula, smiles, or inchi (default: name)' },
494
+ },
495
+ tier: 'free',
496
+ async execute(args) {
497
+ const query = String(args.query).trim();
498
+ const searchType = String(args.search_type || 'name').toLowerCase();
499
+ const nsMap = {
500
+ name: 'name',
501
+ formula: 'formula',
502
+ smiles: 'smiles',
503
+ inchi: 'inchi',
504
+ };
505
+ const ns = nsMap[searchType] || 'name';
506
+ try {
507
+ const encoded = encodeURIComponent(query);
508
+ const url = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/${ns}/${encoded}/JSON`;
509
+ const res = await labFetch(url);
510
+ if (!res.ok) {
511
+ if (res.status === 404)
512
+ return `No compounds found for ${searchType}="${query}" on PubChem.`;
513
+ return `PubChem API error: HTTP ${res.status}`;
514
+ }
515
+ const data = await res.json();
516
+ const compounds = data.PC_Compounds;
517
+ if (!compounds?.length)
518
+ return `No compounds found for ${searchType}="${query}".`;
519
+ const lines = [`## PubChem Search: "${query}" (${searchType})`, ''];
520
+ for (const comp of compounds.slice(0, 5)) {
521
+ const cid = comp.id?.id?.cid || 'unknown';
522
+ lines.push(`### CID: ${cid}`);
523
+ const props = comp.props || [];
524
+ for (const prop of props) {
525
+ const label = prop.urn?.label || '';
526
+ const name = prop.urn?.name || '';
527
+ const val = prop.value?.sval ?? prop.value?.fval ?? prop.value?.ival ?? '';
528
+ if (label === 'IUPAC Name' && name === 'Preferred') {
529
+ lines.push(`- **IUPAC Name**: ${val}`);
530
+ }
531
+ else if (label === 'Molecular Formula') {
532
+ lines.push(`- **Formula**: ${val}`);
533
+ }
534
+ else if (label === 'Molecular Weight') {
535
+ lines.push(`- **MW**: ${val} g/mol`);
536
+ }
537
+ else if (label === 'SMILES' && name === 'Canonical') {
538
+ lines.push(`- **SMILES**: \`${val}\``);
539
+ }
540
+ else if (label === 'InChI' && name === 'Standard') {
541
+ lines.push(`- **InChI**: \`${val}\``);
542
+ }
543
+ else if (label === 'InChIKey' && name === 'Standard') {
544
+ lines.push(`- **InChIKey**: \`${val}\``);
545
+ }
546
+ }
547
+ lines.push(`- **Link**: https://pubchem.ncbi.nlm.nih.gov/compound/${cid}`);
548
+ lines.push('');
549
+ }
550
+ return lines.join('\n');
551
+ }
552
+ catch (e) {
553
+ return `Error searching PubChem: ${e instanceof Error ? e.message : String(e)}`;
554
+ }
555
+ },
556
+ });
557
+ // ── 2. compound_properties ──────────────────────────────────────────────
558
+ registerTool({
559
+ name: 'compound_properties',
560
+ description: 'Get physicochemical properties of a compound from PubChem: molecular weight, XLogP, TPSA, H-bond donors/acceptors, rotatable bonds, complexity, and Lipinski Rule-of-5 evaluation.',
561
+ parameters: {
562
+ identifier: { type: 'string', description: 'Compound name, CID, or SMILES string', required: true },
563
+ identifier_type: { type: 'string', description: 'Type: name, cid, or smiles (default: name)' },
564
+ },
565
+ tier: 'free',
566
+ async execute(args) {
567
+ const identifier = String(args.identifier).trim();
568
+ const idType = String(args.identifier_type || 'name').toLowerCase();
569
+ const nsMap = { name: 'name', cid: 'cid', smiles: 'smiles' };
570
+ const ns = nsMap[idType] || 'name';
571
+ const properties = 'MolecularFormula,MolecularWeight,XLogP,TPSA,HBondDonorCount,HBondAcceptorCount,RotatableBondCount,Complexity,InChIKey';
572
+ try {
573
+ const encoded = encodeURIComponent(identifier);
574
+ const url = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/${ns}/${encoded}/property/${properties}/JSON`;
575
+ const res = await labFetch(url);
576
+ if (!res.ok) {
577
+ if (res.status === 404)
578
+ return `Compound not found: ${identifier}`;
579
+ return `PubChem API error: HTTP ${res.status}`;
580
+ }
581
+ const data = await res.json();
582
+ const props = data.PropertyTable?.Properties?.[0];
583
+ if (!props)
584
+ return `No properties found for "${identifier}".`;
585
+ const mw = props.MolecularWeight ?? 0;
586
+ const xlogp = props.XLogP;
587
+ const hbd = props.HBondDonorCount ?? 0;
588
+ const hba = props.HBondAcceptorCount ?? 0;
589
+ // Lipinski Rule of 5
590
+ const lipinski = {
591
+ mw_ok: mw <= 500,
592
+ logp_ok: xlogp != null ? xlogp <= 5 : true,
593
+ hbd_ok: hbd <= 5,
594
+ hba_ok: hba <= 10,
595
+ };
596
+ const violations = [
597
+ !lipinski.mw_ok ? 'MW > 500' : null,
598
+ !lipinski.logp_ok ? 'LogP > 5' : null,
599
+ !lipinski.hbd_ok ? 'HBD > 5' : null,
600
+ !lipinski.hba_ok ? 'HBA > 10' : null,
601
+ ].filter(Boolean);
602
+ const lipinskiPass = violations.length <= 1;
603
+ const lines = [
604
+ `## Compound Properties: ${identifier}`,
605
+ '',
606
+ `| Property | Value |`,
607
+ `|----------|-------|`,
608
+ `| **CID** | ${props.CID ?? 'N/A'} |`,
609
+ `| **Formula** | ${props.MolecularFormula ?? 'N/A'} |`,
610
+ `| **Molecular Weight** | ${mw.toFixed(2)} g/mol |`,
611
+ `| **XLogP** | ${xlogp != null ? xlogp.toFixed(2) : 'N/A'} |`,
612
+ `| **TPSA** | ${props.TPSA != null ? props.TPSA.toFixed(2) + ' A^2' : 'N/A'} |`,
613
+ `| **H-Bond Donors** | ${hbd} |`,
614
+ `| **H-Bond Acceptors** | ${hba} |`,
615
+ `| **Rotatable Bonds** | ${props.RotatableBondCount ?? 'N/A'} |`,
616
+ `| **Complexity** | ${props.Complexity != null ? props.Complexity.toFixed(1) : 'N/A'} |`,
617
+ `| **InChIKey** | \`${props.InChIKey ?? 'N/A'}\` |`,
618
+ '',
619
+ `### Lipinski Rule of 5`,
620
+ `- **Status**: ${lipinskiPass ? 'PASS' : 'FAIL'} (${violations.length} violation${violations.length !== 1 ? 's' : ''})`,
621
+ ];
622
+ if (violations.length > 0) {
623
+ lines.push(`- **Violations**: ${violations.join(', ')}`);
624
+ }
625
+ lines.push(`- MW <= 500: ${lipinski.mw_ok ? 'Yes' : 'No'} (${mw.toFixed(1)})`);
626
+ lines.push(`- LogP <= 5: ${lipinski.logp_ok ? 'Yes' : 'No'} (${xlogp != null ? xlogp.toFixed(2) : 'N/A'})`);
627
+ lines.push(`- HBD <= 5: ${lipinski.hbd_ok ? 'Yes' : 'No'} (${hbd})`);
628
+ lines.push(`- HBA <= 10: ${lipinski.hba_ok ? 'Yes' : 'No'} (${hba})`);
629
+ return lines.join('\n');
630
+ }
631
+ catch (e) {
632
+ return `Error fetching properties: ${e instanceof Error ? e.message : String(e)}`;
633
+ }
634
+ },
635
+ });
636
+ // ── 3. reaction_lookup ──────────────────────────────────────────────────
637
+ registerTool({
638
+ name: 'reaction_lookup',
639
+ description: 'Search the Rhea database for enzyme-catalyzed biochemical reactions by reactant, product, name, or EC enzyme number.',
640
+ parameters: {
641
+ query: { type: 'string', description: 'Reactant name, product name, reaction name, or EC enzyme number', required: true },
642
+ search_by: { type: 'string', description: 'Search type: reactant, product, name, or enzyme (default: name)' },
643
+ },
644
+ tier: 'free',
645
+ async execute(args) {
646
+ const query = String(args.query).trim();
647
+ const searchBy = String(args.search_by || 'name').toLowerCase();
648
+ try {
649
+ const encoded = encodeURIComponent(query);
650
+ // Rhea REST API: text search supports all types
651
+ const url = `https://www.rhea-db.org/rhea?query=${encoded}&columns=rhea-id,equation,chebi-name,ec&format=tsv&limit=10`;
652
+ const res = await labFetch(url);
653
+ if (!res.ok) {
654
+ return `Rhea API error: HTTP ${res.status}. Try a different query.`;
655
+ }
656
+ const text = await res.text();
657
+ const rows = text.trim().split('\n');
658
+ if (rows.length <= 1)
659
+ return `No reactions found for "${query}" (${searchBy}).`;
660
+ const headers = rows[0].split('\t');
661
+ const lines = [
662
+ `## Rhea Reaction Search: "${query}" (${searchBy})`,
663
+ '',
664
+ ];
665
+ for (const row of rows.slice(1, 11)) {
666
+ const cols = row.split('\t');
667
+ const rheaId = cols[0] || '';
668
+ const equation = cols[1] || '';
669
+ const chebi = cols[2] || '';
670
+ const ec = cols[3] || '';
671
+ lines.push(`### RHEA:${rheaId}`);
672
+ lines.push(`- **Equation**: ${equation}`);
673
+ if (chebi)
674
+ lines.push(`- **ChEBI Names**: ${chebi}`);
675
+ if (ec)
676
+ lines.push(`- **EC Number**: ${ec}`);
677
+ lines.push(`- **Link**: https://www.rhea-db.org/rhea/${rheaId}`);
678
+ lines.push('');
679
+ }
680
+ return lines.join('\n');
681
+ }
682
+ catch (e) {
683
+ return `Error searching Rhea: ${e instanceof Error ? e.message : String(e)}`;
684
+ }
685
+ },
686
+ });
687
+ // ── 4. element_info ─────────────────────────────────────────────────────
688
+ registerTool({
689
+ name: 'element_info',
690
+ description: 'Complete periodic table lookup for any of the 118 elements. Returns atomic number, symbol, name, atomic mass, electron configuration, electronegativity, ionization energy, density, melting/boiling points, crystal structure, discovery year, and category.',
691
+ parameters: {
692
+ element: { type: 'string', description: 'Element symbol (Fe), name (Iron), or atomic number (26)', required: true },
693
+ },
694
+ tier: 'free',
695
+ async execute(args) {
696
+ const query = String(args.element).trim();
697
+ const el = lookupElement(query);
698
+ if (!el) {
699
+ // Try fuzzy match
700
+ const lower = query.toLowerCase();
701
+ const fuzzy = PERIODIC_TABLE.find(e => e.name.toLowerCase().startsWith(lower) || e.symbol.toLowerCase().startsWith(lower));
702
+ if (!fuzzy)
703
+ return `Element not found: "${query}". Provide a symbol (Fe), name (Iron), or atomic number (26).`;
704
+ return formatElement(fuzzy);
705
+ }
706
+ return formatElement(el);
707
+ },
708
+ });
709
+ function formatElement(el) {
710
+ const lines = [
711
+ `## ${el.name} (${el.symbol})`,
712
+ '',
713
+ `| Property | Value |`,
714
+ `|----------|-------|`,
715
+ `| **Atomic Number** | ${el.number} |`,
716
+ `| **Symbol** | ${el.symbol} |`,
717
+ `| **Name** | ${el.name} |`,
718
+ `| **Atomic Mass** | ${el.atomicMass} u |`,
719
+ `| **Category** | ${el.category} |`,
720
+ `| **Electron Configuration** | ${el.electronConfiguration} |`,
721
+ `| **Electronegativity** | ${el.electronegativity ?? 'N/A'} (Pauling) |`,
722
+ `| **Ionization Energy** | ${el.ionizationEnergy != null ? el.ionizationEnergy + ' kJ/mol' : 'N/A'} |`,
723
+ `| **Density** | ${el.density != null ? el.density + ' g/cm\u00B3' : 'N/A'} |`,
724
+ `| **Melting Point** | ${el.meltingPoint != null ? el.meltingPoint + ' K (' + (el.meltingPoint - 273.15).toFixed(1) + ' \u00B0C)' : 'N/A'} |`,
725
+ `| **Boiling Point** | ${el.boilingPoint != null ? el.boilingPoint + ' K (' + (el.boilingPoint - 273.15).toFixed(1) + ' \u00B0C)' : 'N/A'} |`,
726
+ `| **Crystal Structure** | ${el.crystalStructure ?? 'N/A'} |`,
727
+ `| **Discovery Year** | ${el.discoveryYear ?? 'Ancient'} |`,
728
+ ];
729
+ return lines.join('\n');
730
+ }
731
+ // ── 5. material_properties ──────────────────────────────────────────────
732
+ registerTool({
733
+ name: 'material_properties',
734
+ description: 'Search the Materials Project database for material data: band gap, formation energy, density, space group, and symmetry. Requires MP_API_KEY environment variable.',
735
+ parameters: {
736
+ formula: { type: 'string', description: 'Chemical formula (e.g., "Fe2O3", "GaN", "SiC")', required: true },
737
+ property: { type: 'string', description: 'Focus property: band_gap, elasticity, density, or formation_energy (optional, returns all by default)' },
738
+ },
739
+ tier: 'free',
740
+ async execute(args) {
741
+ const formula = String(args.formula).trim();
742
+ const property = args.property ? String(args.property).trim() : null;
743
+ const apiKey = process.env.MP_API_KEY;
744
+ if (!apiKey) {
745
+ return [
746
+ `## Materials Project: ${formula}`,
747
+ '',
748
+ '**API key required.** Set the `MP_API_KEY` environment variable to use this tool.',
749
+ '',
750
+ 'Get a free API key at: https://materialsproject.org/api',
751
+ '',
752
+ '```bash',
753
+ 'export MP_API_KEY="your_key_here"',
754
+ '```',
755
+ ].join('\n');
756
+ }
757
+ try {
758
+ const encoded = encodeURIComponent(formula);
759
+ const fields = 'material_id,formula_pretty,band_gap,formation_energy_per_atom,density,symmetry';
760
+ const url = `https://api.materialsproject.org/materials/summary/?formula=${encoded}&_fields=${fields}&_limit=5`;
761
+ const res = await fetch(url, {
762
+ headers: {
763
+ 'User-Agent': UA,
764
+ 'X-API-KEY': apiKey,
765
+ Accept: 'application/json',
766
+ },
767
+ signal: AbortSignal.timeout(10000),
768
+ });
769
+ if (!res.ok) {
770
+ return `Materials Project API error: HTTP ${res.status}. Check your API key.`;
771
+ }
772
+ const data = await res.json();
773
+ const results = data.data;
774
+ if (!results?.length)
775
+ return `No materials found for formula "${formula}".`;
776
+ const lines = [`## Materials Project: ${formula}`, ''];
777
+ for (const mat of results) {
778
+ lines.push(`### ${mat.material_id ?? 'unknown'} — ${mat.formula_pretty ?? formula}`);
779
+ lines.push('');
780
+ lines.push(`| Property | Value |`);
781
+ lines.push(`|----------|-------|`);
782
+ if (!property || property === 'band_gap') {
783
+ lines.push(`| **Band Gap** | ${mat.band_gap != null ? mat.band_gap.toFixed(3) + ' eV' : 'N/A'} |`);
784
+ }
785
+ if (!property || property === 'formation_energy') {
786
+ lines.push(`| **Formation Energy** | ${mat.formation_energy_per_atom != null ? mat.formation_energy_per_atom.toFixed(4) + ' eV/atom' : 'N/A'} |`);
787
+ }
788
+ if (!property || property === 'density') {
789
+ lines.push(`| **Density** | ${mat.density != null ? mat.density.toFixed(3) + ' g/cm\u00B3' : 'N/A'} |`);
790
+ }
791
+ if (mat.symmetry) {
792
+ lines.push(`| **Crystal System** | ${mat.symmetry.crystal_system ?? 'N/A'} |`);
793
+ lines.push(`| **Space Group** | ${mat.symmetry.symbol ?? 'N/A'} (${mat.symmetry.number ?? ''}) |`);
794
+ }
795
+ lines.push(`| **Link** | https://materialsproject.org/materials/${mat.material_id} |`);
796
+ lines.push('');
797
+ }
798
+ return lines.join('\n');
799
+ }
800
+ catch (e) {
801
+ return `Error querying Materials Project: ${e instanceof Error ? e.message : String(e)}`;
802
+ }
803
+ },
804
+ });
805
+ // ── 6. spectroscopy_lookup ──────────────────────────────────────────────
806
+ registerTool({
807
+ name: 'spectroscopy_lookup',
808
+ description: 'Look up spectroscopy reference data from the NIST Chemistry WebBook. Supports IR (infrared), MS (mass spectrometry), and UV-Vis spectra. Returns NIST WebBook links and available data.',
809
+ parameters: {
810
+ compound: { type: 'string', description: 'Compound name (e.g., "ethanol", "benzene", "acetone")', required: true },
811
+ spectrum_type: { type: 'string', description: 'Spectrum type: ir, ms, or uv (default: ms)' },
812
+ },
813
+ tier: 'free',
814
+ async execute(args) {
815
+ const compound = String(args.compound).trim();
816
+ const specType = String(args.spectrum_type || 'ms').toLowerCase();
817
+ // NIST WebBook mask values: 1=thermo, 2=phase, 80=IR, 200=MS, 400=UV
818
+ const maskMap = { ir: 80, ms: 200, uv: 400 };
819
+ const mask = maskMap[specType] ?? 200;
820
+ const typeLabel = { ir: 'Infrared (IR)', ms: 'Mass Spectrum (MS)', uv: 'UV-Vis' };
821
+ try {
822
+ const encoded = encodeURIComponent(compound);
823
+ const url = `https://webbook.nist.gov/cgi/cbook.cgi?Name=${encoded}&Units=SI&Mask=${mask}`;
824
+ const res = await fetch(url, {
825
+ headers: { 'User-Agent': UA, Accept: 'text/html,*/*' },
826
+ signal: AbortSignal.timeout(10000),
827
+ redirect: 'follow',
828
+ });
829
+ if (!res.ok) {
830
+ return `NIST WebBook error: HTTP ${res.status}`;
831
+ }
832
+ const html = await res.text();
833
+ // Extract key information from HTML
834
+ const lines = [
835
+ `## NIST WebBook: ${compound} — ${typeLabel[specType] ?? specType}`,
836
+ '',
837
+ ];
838
+ // Try to extract the compound name/CAS from the page title
839
+ const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
840
+ if (titleMatch) {
841
+ lines.push(`**Page**: ${titleMatch[1].trim()}`);
842
+ }
843
+ // Extract CAS number if present
844
+ const casMatch = html.match(/CAS Registry Number:\s*<[^>]*>([^<]+)/i) ||
845
+ html.match(/(\d{2,7}-\d{2}-\d)/i);
846
+ if (casMatch) {
847
+ lines.push(`**CAS**: ${casMatch[1].trim()}`);
848
+ }
849
+ // Extract molecular formula
850
+ const formulaMatch = html.match(/Molecular formula:\s*<[^>]*>([^<]+)/i) ||
851
+ html.match(/Formula:\s*([A-Z][A-Za-z0-9]+)/i);
852
+ if (formulaMatch) {
853
+ lines.push(`**Formula**: ${formulaMatch[1].trim()}`);
854
+ }
855
+ // Check if spectrum data is available on the page
856
+ if (specType === 'ms' && html.includes('Mass spectrum')) {
857
+ lines.push('', '**Mass spectrum data available.**');
858
+ // Try to extract peak information
859
+ const peakSection = html.match(/mass spectrum[^]*?(?=<h[23]|$)/i);
860
+ if (peakSection) {
861
+ const peakHits = peakSection[0].match(/(\d+)\s*\(\s*(\d+)\s*\)/g);
862
+ if (peakHits?.length) {
863
+ lines.push('', '**Major peaks** (m/z, relative intensity):');
864
+ for (const p of peakHits.slice(0, 15)) {
865
+ lines.push(`- ${p}`);
866
+ }
867
+ }
868
+ }
869
+ }
870
+ else if (specType === 'ir' && html.includes('IR Spectrum')) {
871
+ lines.push('', '**IR spectrum data available.**');
872
+ }
873
+ else if (specType === 'uv' && html.includes('UV/Vis')) {
874
+ lines.push('', '**UV-Vis spectrum data available.**');
875
+ }
876
+ else {
877
+ lines.push('', `No ${typeLabel[specType] ?? specType} data found for this compound.`);
878
+ }
879
+ // Always include the direct link
880
+ lines.push('', `**NIST WebBook link**: ${url}`);
881
+ lines.push(`**Full entry**: https://webbook.nist.gov/cgi/cbook.cgi?Name=${encoded}&Units=SI`);
882
+ return lines.join('\n');
883
+ }
884
+ catch (e) {
885
+ return `Error querying NIST WebBook: ${e instanceof Error ? e.message : String(e)}`;
886
+ }
887
+ },
888
+ });
889
+ // ── 7. chemical_safety ──────────────────────────────────────────────────
890
+ registerTool({
891
+ name: 'chemical_safety',
892
+ description: 'Get GHS (Globally Harmonized System) hazard classification data from PubChem: pictograms, signal word, hazard statements, and precautionary statements.',
893
+ parameters: {
894
+ compound: { type: 'string', description: 'Compound name (e.g., "methanol", "hydrochloric acid")', required: true },
895
+ },
896
+ tier: 'free',
897
+ async execute(args) {
898
+ const compound = String(args.compound).trim();
899
+ try {
900
+ // Step 1: resolve compound name to CID
901
+ const cidUrl = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/${encodeURIComponent(compound)}/cids/JSON`;
902
+ const cidRes = await labFetch(cidUrl);
903
+ if (!cidRes.ok) {
904
+ return `Compound not found on PubChem: "${compound}"`;
905
+ }
906
+ const cidData = await cidRes.json();
907
+ const cid = cidData.IdentifierList?.CID?.[0];
908
+ if (!cid)
909
+ return `No CID found for "${compound}".`;
910
+ // Step 2: get GHS classification
911
+ const ghsUrl = `https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/compound/${cid}/JSON?heading=GHS+Classification`;
912
+ const ghsRes = await labFetch(ghsUrl);
913
+ if (!ghsRes.ok) {
914
+ return [
915
+ `## Safety Data: ${compound} (CID: ${cid})`,
916
+ '',
917
+ 'No GHS classification data available on PubChem.',
918
+ `Check: https://pubchem.ncbi.nlm.nih.gov/compound/${cid}#section=Safety-and-Hazards`,
919
+ ].join('\n');
920
+ }
921
+ const ghsData = await ghsRes.json();
922
+ const lines = [
923
+ `## GHS Safety Data: ${compound} (CID: ${cid})`,
924
+ '',
925
+ ];
926
+ // Parse GHS sections
927
+ const sections = ghsData.Record?.Section || [];
928
+ for (const section of sections) {
929
+ const innerSections = section.Section || [];
930
+ for (const inner of innerSections) {
931
+ const heading = inner.TOCHeading || '';
932
+ const infos = inner.Information || [];
933
+ for (const info of infos) {
934
+ const name = info.Name || '';
935
+ const strings = info.Value?.StringWithMarkup?.map(s => s.String).filter(Boolean) || [];
936
+ const markups = info.Value?.StringWithMarkup?.flatMap(s => s.Markup || []) || [];
937
+ if (name === 'Pictogram(s)' || heading.includes('Pictogram')) {
938
+ const pictoNames = markups.map(m => m.Extra).filter(Boolean);
939
+ if (pictoNames.length) {
940
+ lines.push(`### Pictograms`);
941
+ for (const p of pictoNames)
942
+ lines.push(`- ${p}`);
943
+ lines.push('');
944
+ }
945
+ }
946
+ else if (name === 'Signal' || heading.includes('Signal')) {
947
+ if (strings.length) {
948
+ lines.push(`### Signal Word`);
949
+ lines.push(strings.join(', '));
950
+ lines.push('');
951
+ }
952
+ }
953
+ else if (name.includes('Hazard Statement') || heading.includes('Hazard Statement')) {
954
+ if (strings.length) {
955
+ lines.push(`### Hazard Statements`);
956
+ for (const s of strings)
957
+ lines.push(`- ${s}`);
958
+ lines.push('');
959
+ }
960
+ }
961
+ else if (name.includes('Precautionary Statement') || heading.includes('Precautionary')) {
962
+ if (strings.length) {
963
+ lines.push(`### Precautionary Statements`);
964
+ for (const s of strings)
965
+ lines.push(`- ${s}`);
966
+ lines.push('');
967
+ }
968
+ }
969
+ else if (strings.length && name) {
970
+ lines.push(`### ${name}`);
971
+ for (const s of strings)
972
+ lines.push(`- ${s}`);
973
+ lines.push('');
974
+ }
975
+ }
976
+ }
977
+ }
978
+ lines.push(`**Full safety data**: https://pubchem.ncbi.nlm.nih.gov/compound/${cid}#section=Safety-and-Hazards`);
979
+ return lines.join('\n');
980
+ }
981
+ catch (e) {
982
+ return `Error fetching safety data: ${e instanceof Error ? e.message : String(e)}`;
983
+ }
984
+ },
985
+ });
986
+ // ── 8. stoichiometry_calc ───────────────────────────────────────────────
987
+ registerTool({
988
+ name: 'stoichiometry_calc',
989
+ description: 'Balance chemical equations using Gaussian elimination and calculate stoichiometric quantities. Parses chemical formulas including parenthetical groups (Ca(OH)2) and hydrates (CuSO4·5H2O). Can compute moles, grams, and identify the limiting reagent.',
990
+ parameters: {
991
+ equation: { type: 'string', description: 'Chemical equation (e.g., "Fe + O2 -> Fe2O3")', required: true },
992
+ known_quantity: { type: 'string', description: 'Known quantity for stoichiometry (e.g., "Fe=10g" or "O2=3mol"). Optional.' },
993
+ },
994
+ tier: 'free',
995
+ async execute(args) {
996
+ const equation = String(args.equation).trim();
997
+ const knownQty = args.known_quantity ? String(args.known_quantity).trim() : null;
998
+ const result = balanceEquation(equation);
999
+ if (typeof result === 'string')
1000
+ return result;
1001
+ const { reactants, products, coefficients } = result;
1002
+ const allCompounds = [...reactants, ...products];
1003
+ // Format balanced equation
1004
+ const reactantStr = reactants
1005
+ .map((r, i) => coefficients[i] === 1 ? r : `${coefficients[i]}${r}`)
1006
+ .join(' + ');
1007
+ const productStr = products
1008
+ .map((p, i) => {
1009
+ const ci = coefficients[reactants.length + i];
1010
+ return ci === 1 ? p : `${ci}${p}`;
1011
+ })
1012
+ .join(' + ');
1013
+ const lines = [
1014
+ `## Balanced Equation`,
1015
+ '',
1016
+ `**${reactantStr} \u2192 ${productStr}**`,
1017
+ '',
1018
+ `### Coefficients`,
1019
+ ];
1020
+ for (let i = 0; i < allCompounds.length; i++) {
1021
+ const mw = formulaWeight(allCompounds[i]);
1022
+ const counts = parseFormula(allCompounds[i]);
1023
+ lines.push(`- **${allCompounds[i]}**: coefficient = ${coefficients[i]}, MW = ${mw.toFixed(2)} g/mol, formula = ${countsToFormula(counts)}`);
1024
+ }
1025
+ // Stoichiometry calculation if known_quantity provided
1026
+ if (knownQty) {
1027
+ const match = knownQty.match(/^([A-Za-z0-9()]+)\s*=\s*([\d.]+)\s*(g|mol|kg|mg)$/i);
1028
+ if (match) {
1029
+ const knownCompound = match[1];
1030
+ const knownValue = parseFloat(match[2]);
1031
+ const knownUnit = match[3].toLowerCase();
1032
+ // Find the compound index
1033
+ const idx = allCompounds.findIndex(c => c.toLowerCase() === knownCompound.toLowerCase());
1034
+ if (idx >= 0) {
1035
+ const mwKnown = formulaWeight(allCompounds[idx]);
1036
+ let moles;
1037
+ if (knownUnit === 'mol') {
1038
+ moles = knownValue;
1039
+ }
1040
+ else if (knownUnit === 'g') {
1041
+ moles = knownValue / mwKnown;
1042
+ }
1043
+ else if (knownUnit === 'kg') {
1044
+ moles = (knownValue * 1000) / mwKnown;
1045
+ }
1046
+ else { // mg
1047
+ moles = (knownValue / 1000) / mwKnown;
1048
+ }
1049
+ lines.push('');
1050
+ lines.push(`### Stoichiometry (given ${knownCompound} = ${knownValue} ${knownUnit})`);
1051
+ lines.push(`- ${knownCompound}: ${moles.toFixed(4)} mol = ${(moles * mwKnown).toFixed(4)} g`);
1052
+ lines.push('');
1053
+ lines.push(`| Compound | Moles | Grams | Role |`);
1054
+ lines.push(`|----------|-------|-------|------|`);
1055
+ for (let i = 0; i < allCompounds.length; i++) {
1056
+ const mw = formulaWeight(allCompounds[i]);
1057
+ const stoichMoles = moles * coefficients[i] / coefficients[idx];
1058
+ const role = i < reactants.length ? 'Reactant' : 'Product';
1059
+ lines.push(`| ${allCompounds[i]} | ${stoichMoles.toFixed(4)} | ${(stoichMoles * mw).toFixed(4)} | ${role} |`);
1060
+ }
1061
+ }
1062
+ else {
1063
+ lines.push('', `*Compound "${knownCompound}" not found in equation.*`);
1064
+ }
1065
+ }
1066
+ else {
1067
+ lines.push('', `*Could not parse quantity "${knownQty}". Use format: "Fe=10g" or "O2=3mol".*`);
1068
+ }
1069
+ }
1070
+ // Show element breakdown verification
1071
+ lines.push('');
1072
+ lines.push('### Atom Balance Verification');
1073
+ const allElements = new Set();
1074
+ const parsed = allCompounds.map(c => parseFormula(c));
1075
+ for (const p of parsed) {
1076
+ for (const el of Object.keys(p))
1077
+ allElements.add(el);
1078
+ }
1079
+ let balanced = true;
1080
+ for (const el of allElements) {
1081
+ let lhs = 0;
1082
+ let rhs = 0;
1083
+ for (let i = 0; i < reactants.length; i++) {
1084
+ lhs += (parsed[i][el] || 0) * coefficients[i];
1085
+ }
1086
+ for (let i = 0; i < products.length; i++) {
1087
+ rhs += (parsed[reactants.length + i][el] || 0) * coefficients[reactants.length + i];
1088
+ }
1089
+ const ok = lhs === rhs;
1090
+ if (!ok)
1091
+ balanced = false;
1092
+ lines.push(`- ${el}: LHS=${lhs}, RHS=${rhs} ${ok ? '\u2713' : '\u2717'}`);
1093
+ }
1094
+ lines.push('');
1095
+ lines.push(balanced ? '**Equation is balanced.**' : '**Warning: equation may not be fully balanced.**');
1096
+ return lines.join('\n');
1097
+ },
1098
+ });
1099
+ // ── 9. crystal_structure ────────────────────────────────────────────────
1100
+ registerTool({
1101
+ name: 'crystal_structure',
1102
+ description: 'Search the Crystallography Open Database (COD) for crystal structures by formula, mineral name, or text query. Returns cell parameters, space group, and structure details.',
1103
+ parameters: {
1104
+ query: { type: 'string', description: 'Chemical formula ("SiO2"), mineral name ("quartz"), or text query', required: true },
1105
+ search_type: { type: 'string', description: 'Search type: formula, mineral, or text (default: formula)' },
1106
+ },
1107
+ tier: 'free',
1108
+ async execute(args) {
1109
+ const query = String(args.query).trim();
1110
+ const searchType = String(args.search_type || 'formula').toLowerCase();
1111
+ try {
1112
+ let url;
1113
+ const encoded = encodeURIComponent(query);
1114
+ if (searchType === 'mineral') {
1115
+ url = `https://www.crystallography.net/cod/result?mineral=${encoded}&format=json`;
1116
+ }
1117
+ else if (searchType === 'text') {
1118
+ url = `https://www.crystallography.net/cod/result?text=${encoded}&format=json`;
1119
+ }
1120
+ else {
1121
+ url = `https://www.crystallography.net/cod/result?formula=${encoded}&format=json`;
1122
+ }
1123
+ const res = await labFetch(url);
1124
+ if (!res.ok) {
1125
+ return `COD API error: HTTP ${res.status}`;
1126
+ }
1127
+ const data = await res.json();
1128
+ const entries = Object.entries(data);
1129
+ if (entries.length === 0)
1130
+ return `No crystal structures found for "${query}" (${searchType}).`;
1131
+ const lines = [
1132
+ `## COD Crystal Structure Search: "${query}" (${searchType})`,
1133
+ `Found ${entries.length} result${entries.length !== 1 ? 's' : ''}. Showing first ${Math.min(entries.length, 5)}.`,
1134
+ '',
1135
+ ];
1136
+ for (const [codId, entry] of entries.slice(0, 5)) {
1137
+ lines.push(`### COD ${codId}`);
1138
+ if (entry.formula)
1139
+ lines.push(`- **Formula**: ${entry.formula}`);
1140
+ if (entry.mineral)
1141
+ lines.push(`- **Mineral**: ${entry.mineral}`);
1142
+ if (entry.sg)
1143
+ lines.push(`- **Space Group**: ${entry.sg}${entry.sgHall ? ` (Hall: ${entry.sgHall})` : ''}`);
1144
+ if (entry.a != null) {
1145
+ lines.push(`- **Cell Parameters**: a=${entry.a}, b=${entry.b}, c=${entry.c} A`);
1146
+ lines.push(` - alpha=${entry.alpha}\u00B0, beta=${entry.beta}\u00B0, gamma=${entry.gamma}\u00B0`);
1147
+ }
1148
+ if (entry.vol != null)
1149
+ lines.push(`- **Volume**: ${entry.vol} A\u00B3`);
1150
+ if (entry.title)
1151
+ lines.push(`- **Title**: ${entry.title}`);
1152
+ if (entry.authors)
1153
+ lines.push(`- **Authors**: ${entry.authors}`);
1154
+ if (entry.journal && entry.year)
1155
+ lines.push(`- **Reference**: ${entry.journal} (${entry.year})`);
1156
+ if (entry.Rall != null)
1157
+ lines.push(`- **R-factor**: ${entry.Rall}`);
1158
+ lines.push(`- **Link**: https://www.crystallography.net/cod/${codId}.html`);
1159
+ lines.push('');
1160
+ }
1161
+ return lines.join('\n');
1162
+ }
1163
+ catch (e) {
1164
+ return `Error querying COD: ${e instanceof Error ? e.message : String(e)}`;
1165
+ }
1166
+ },
1167
+ });
1168
+ // ── 10. thermodynamics_data ─────────────────────────────────────────────
1169
+ registerTool({
1170
+ name: 'thermodynamics_data',
1171
+ description: 'Look up standard thermodynamic properties: enthalpy of formation (deltaHf), entropy (S), heat capacity (Cp), and Gibbs free energy (deltaGf). Uses embedded data for ~100 common substances as primary source, with NIST WebBook as supplementary lookup.',
1172
+ parameters: {
1173
+ substance: { type: 'string', description: 'Chemical formula or name (e.g., "H2O(l)", "methane", "NaCl")', required: true },
1174
+ temperature: { type: 'number', description: 'Temperature in Kelvin (default: 298.15). Embedded data is for 298.15 K only.' },
1175
+ },
1176
+ tier: 'free',
1177
+ async execute(args) {
1178
+ const substance = String(args.substance).trim();
1179
+ const temp = typeof args.temperature === 'number' ? args.temperature : 298.15;
1180
+ // Try embedded table first
1181
+ const local = lookupThermo(substance);
1182
+ if (local) {
1183
+ const lines = [
1184
+ `## Thermodynamic Data: ${local.name}`,
1185
+ `**Formula**: ${local.formula} | **T** = 298.15 K (standard state)`,
1186
+ '',
1187
+ `| Property | Value |`,
1188
+ `|----------|-------|`,
1189
+ `| **\u0394H\u00B0f** (enthalpy of formation) | ${local.deltaHf != null ? local.deltaHf.toFixed(2) + ' kJ/mol' : 'N/A'} |`,
1190
+ `| **S\u00B0** (standard molar entropy) | ${local.S != null ? local.S.toFixed(2) + ' J/(mol\u00B7K)' : 'N/A'} |`,
1191
+ `| **Cp\u00B0** (heat capacity) | ${local.Cp != null ? local.Cp.toFixed(2) + ' J/(mol\u00B7K)' : 'N/A'} |`,
1192
+ `| **\u0394G\u00B0f** (Gibbs free energy) | ${local.deltaGf != null ? local.deltaGf.toFixed(2) + ' kJ/mol' : 'N/A'} |`,
1193
+ ];
1194
+ if (temp !== 298.15 && local.Cp != null && local.deltaHf != null && local.S != null) {
1195
+ // Approximate at different temperature using Kirchhoff's equation
1196
+ const dT = temp - 298.15;
1197
+ const approxH = local.deltaHf + (local.Cp / 1000) * dT;
1198
+ const approxS = local.S + local.Cp * Math.log(temp / 298.15);
1199
+ const approxG = approxH - (temp * approxS / 1000);
1200
+ lines.push('');
1201
+ lines.push(`### Approximate Values at ${temp} K`);
1202
+ lines.push(`*(Kirchhoff approximation, assuming constant Cp)*`);
1203
+ lines.push(`- \u0394H\u00B0 \u2248 ${approxH.toFixed(2)} kJ/mol`);
1204
+ lines.push(`- S\u00B0 \u2248 ${approxS.toFixed(2)} J/(mol\u00B7K)`);
1205
+ lines.push(`- \u0394G\u00B0 \u2248 ${approxG.toFixed(2)} kJ/mol`);
1206
+ }
1207
+ return lines.join('\n');
1208
+ }
1209
+ // Fall back to NIST WebBook
1210
+ try {
1211
+ const encoded = encodeURIComponent(substance);
1212
+ const url = `https://webbook.nist.gov/cgi/cbook.cgi?Name=${encoded}&Units=SI&Mask=1`;
1213
+ const res = await fetch(url, {
1214
+ headers: { 'User-Agent': UA, Accept: 'text/html,*/*' },
1215
+ signal: AbortSignal.timeout(10000),
1216
+ redirect: 'follow',
1217
+ });
1218
+ if (!res.ok) {
1219
+ return `Substance "${substance}" not found in embedded database. NIST WebBook returned HTTP ${res.status}.`;
1220
+ }
1221
+ const html = await res.text();
1222
+ const lines = [
1223
+ `## Thermodynamic Data: ${substance}`,
1224
+ `*Source: NIST Chemistry WebBook*`,
1225
+ '',
1226
+ ];
1227
+ // Extract title
1228
+ const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
1229
+ if (titleMatch)
1230
+ lines.push(`**Compound**: ${titleMatch[1].trim()}`);
1231
+ // Try to extract standard thermodynamic values from the HTML
1232
+ const deltaHMatch = html.match(/f<\/sub>\s*=?\s*([-\d.]+)\s*(?:kJ\/mol|kJ\s*mol)/i) ||
1233
+ html.match(/([-\d.]+)\s*(?:\u00B1\s*[\d.]+)?\s*kJ\/mol/i);
1234
+ if (deltaHMatch)
1235
+ lines.push(`**\u0394H\u00B0f**: ${deltaHMatch[1]} kJ/mol`);
1236
+ const entropyMatch = html.match(/entropy[^<]*?(\d+\.?\d*)\s*J\/mol/i);
1237
+ if (entropyMatch)
1238
+ lines.push(`**S\u00B0**: ${entropyMatch[1]} J/(mol\u00B7K)`);
1239
+ const cpMatch = html.match(/heat capacity[^<]*?(\d+\.?\d*)\s*J\/mol/i) ||
1240
+ html.match(/Cp[^<]*?(\d+\.?\d*)\s*J/i);
1241
+ if (cpMatch)
1242
+ lines.push(`**Cp\u00B0**: ${cpMatch[1]} J/(mol\u00B7K)`);
1243
+ lines.push('');
1244
+ lines.push(`**NIST WebBook**: ${url}`);
1245
+ if (!deltaHMatch && !entropyMatch && !cpMatch) {
1246
+ lines.push('');
1247
+ lines.push('No thermodynamic data could be extracted from the NIST page. Visit the link above for full data.');
1248
+ }
1249
+ return lines.join('\n');
1250
+ }
1251
+ catch (e) {
1252
+ return `Substance "${substance}" not found in embedded database (${THERMO_TABLE.length} entries). NIST lookup failed: ${e instanceof Error ? e.message : String(e)}`;
1253
+ }
1254
+ },
1255
+ });
1256
+ }
1257
+ //# sourceMappingURL=lab-chem.js.map