@iflow-mcp/kitfunso-luminus 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +488 -0
  5. package/dist/lib/audit.d.ts +3 -0
  6. package/dist/lib/audit.js +66 -0
  7. package/dist/lib/auth.d.ts +26 -0
  8. package/dist/lib/auth.js +199 -0
  9. package/dist/lib/cache.d.ts +25 -0
  10. package/dist/lib/cache.js +38 -0
  11. package/dist/lib/cli.d.ts +1 -0
  12. package/dist/lib/cli.js +10 -0
  13. package/dist/lib/corine.d.ts +31 -0
  14. package/dist/lib/corine.js +137 -0
  15. package/dist/lib/eea-natura2000.d.ts +7 -0
  16. package/dist/lib/eea-natura2000.js +53 -0
  17. package/dist/lib/entsoe-client.d.ts +22 -0
  18. package/dist/lib/entsoe-client.js +69 -0
  19. package/dist/lib/gis-sources.d.ts +33 -0
  20. package/dist/lib/gis-sources.js +392 -0
  21. package/dist/lib/natural-england.d.ts +27 -0
  22. package/dist/lib/natural-england.js +105 -0
  23. package/dist/lib/neso-gsp.d.ts +18 -0
  24. package/dist/lib/neso-gsp.js +113 -0
  25. package/dist/lib/overpass.d.ts +13 -0
  26. package/dist/lib/overpass.js +193 -0
  27. package/dist/lib/profiles.d.ts +23 -0
  28. package/dist/lib/profiles.js +149 -0
  29. package/dist/lib/schema-guard.d.ts +22 -0
  30. package/dist/lib/schema-guard.js +38 -0
  31. package/dist/lib/tool-handler.d.ts +15 -0
  32. package/dist/lib/tool-handler.js +95 -0
  33. package/dist/lib/xml-parser.d.ts +4 -0
  34. package/dist/lib/xml-parser.js +34 -0
  35. package/dist/lib/zone-codes.d.ts +12 -0
  36. package/dist/lib/zone-codes.js +127 -0
  37. package/dist/tools/acer-remit.d.ts +60 -0
  38. package/dist/tools/acer-remit.js +154 -0
  39. package/dist/tools/agricultural-land.d.ts +31 -0
  40. package/dist/tools/agricultural-land.js +210 -0
  41. package/dist/tools/ancillary-prices.d.ts +27 -0
  42. package/dist/tools/ancillary-prices.js +70 -0
  43. package/dist/tools/auctions.d.ts +15 -0
  44. package/dist/tools/auctions.js +89 -0
  45. package/dist/tools/balancing-actions.d.ts +22 -0
  46. package/dist/tools/balancing-actions.js +151 -0
  47. package/dist/tools/balancing.d.ts +21 -0
  48. package/dist/tools/balancing.js +56 -0
  49. package/dist/tools/carbon.d.ts +21 -0
  50. package/dist/tools/carbon.js +68 -0
  51. package/dist/tools/commodity-prices.d.ts +26 -0
  52. package/dist/tools/commodity-prices.js +100 -0
  53. package/dist/tools/compare-sites.d.ts +41 -0
  54. package/dist/tools/compare-sites.js +237 -0
  55. package/dist/tools/demand-forecast.d.ts +21 -0
  56. package/dist/tools/demand-forecast.js +56 -0
  57. package/dist/tools/elexon-bmrs.d.ts +72 -0
  58. package/dist/tools/elexon-bmrs.js +117 -0
  59. package/dist/tools/energi-data.d.ts +72 -0
  60. package/dist/tools/energi-data.js +170 -0
  61. package/dist/tools/energy-charts.d.ts +103 -0
  62. package/dist/tools/energy-charts.js +411 -0
  63. package/dist/tools/entsog.d.ts +71 -0
  64. package/dist/tools/entsog.js +159 -0
  65. package/dist/tools/era5-weather.d.ts +39 -0
  66. package/dist/tools/era5-weather.js +117 -0
  67. package/dist/tools/eu-gas-price.d.ts +38 -0
  68. package/dist/tools/eu-gas-price.js +110 -0
  69. package/dist/tools/fingrid.d.ts +39 -0
  70. package/dist/tools/fingrid.js +158 -0
  71. package/dist/tools/flood-risk.d.ts +33 -0
  72. package/dist/tools/flood-risk.js +166 -0
  73. package/dist/tools/flows.d.ts +23 -0
  74. package/dist/tools/flows.js +61 -0
  75. package/dist/tools/frequency.d.ts +10 -0
  76. package/dist/tools/frequency.js +35 -0
  77. package/dist/tools/gas-storage.d.ts +18 -0
  78. package/dist/tools/gas-storage.js +72 -0
  79. package/dist/tools/generation.d.ts +17 -0
  80. package/dist/tools/generation.js +80 -0
  81. package/dist/tools/grid-connection-intelligence.d.ts +42 -0
  82. package/dist/tools/grid-connection-intelligence.js +122 -0
  83. package/dist/tools/grid-connection-queue.d.ts +64 -0
  84. package/dist/tools/grid-connection-queue.js +198 -0
  85. package/dist/tools/grid-proximity.d.ts +38 -0
  86. package/dist/tools/grid-proximity.js +123 -0
  87. package/dist/tools/hydro-inflows.d.ts +34 -0
  88. package/dist/tools/hydro-inflows.js +114 -0
  89. package/dist/tools/hydro.d.ts +18 -0
  90. package/dist/tools/hydro.js +85 -0
  91. package/dist/tools/imbalance-prices.d.ts +21 -0
  92. package/dist/tools/imbalance-prices.js +56 -0
  93. package/dist/tools/intraday-prices.d.ts +21 -0
  94. package/dist/tools/intraday-prices.js +57 -0
  95. package/dist/tools/intraday-spread.d.ts +24 -0
  96. package/dist/tools/intraday-spread.js +55 -0
  97. package/dist/tools/land-constraints.d.ts +25 -0
  98. package/dist/tools/land-constraints.js +148 -0
  99. package/dist/tools/land-cover.d.ts +18 -0
  100. package/dist/tools/land-cover.js +64 -0
  101. package/dist/tools/lng-terminals.d.ts +22 -0
  102. package/dist/tools/lng-terminals.js +75 -0
  103. package/dist/tools/net-positions.d.ts +19 -0
  104. package/dist/tools/net-positions.js +74 -0
  105. package/dist/tools/nordpool-prices.d.ts +29 -0
  106. package/dist/tools/nordpool-prices.js +80 -0
  107. package/dist/tools/outages.d.ts +28 -0
  108. package/dist/tools/outages.js +107 -0
  109. package/dist/tools/power-plants.d.ts +26 -0
  110. package/dist/tools/power-plants.js +224 -0
  111. package/dist/tools/price-spread-analysis.d.ts +27 -0
  112. package/dist/tools/price-spread-analysis.js +97 -0
  113. package/dist/tools/prices.d.ts +23 -0
  114. package/dist/tools/prices.js +79 -0
  115. package/dist/tools/realtime-generation.d.ts +19 -0
  116. package/dist/tools/realtime-generation.js +141 -0
  117. package/dist/tools/ree-esios.d.ts +78 -0
  118. package/dist/tools/ree-esios.js +216 -0
  119. package/dist/tools/regelleistung.d.ts +28 -0
  120. package/dist/tools/regelleistung.js +71 -0
  121. package/dist/tools/remit-messages.d.ts +23 -0
  122. package/dist/tools/remit-messages.js +110 -0
  123. package/dist/tools/renewable-forecast.d.ts +23 -0
  124. package/dist/tools/renewable-forecast.js +75 -0
  125. package/dist/tools/rte-france.d.ts +72 -0
  126. package/dist/tools/rte-france.js +147 -0
  127. package/dist/tools/screen-site.d.ts +50 -0
  128. package/dist/tools/screen-site.js +288 -0
  129. package/dist/tools/site-revenue.d.ts +50 -0
  130. package/dist/tools/site-revenue.js +147 -0
  131. package/dist/tools/smard-data.d.ts +34 -0
  132. package/dist/tools/smard-data.js +155 -0
  133. package/dist/tools/solar.d.ts +23 -0
  134. package/dist/tools/solar.js +69 -0
  135. package/dist/tools/stormglass.d.ts +56 -0
  136. package/dist/tools/stormglass.js +172 -0
  137. package/dist/tools/terna.d.ts +69 -0
  138. package/dist/tools/terna.js +159 -0
  139. package/dist/tools/terrain-analysis.d.ts +19 -0
  140. package/dist/tools/terrain-analysis.js +120 -0
  141. package/dist/tools/transfer-capacity.d.ts +22 -0
  142. package/dist/tools/transfer-capacity.js +61 -0
  143. package/dist/tools/transmission.d.ts +29 -0
  144. package/dist/tools/transmission.js +159 -0
  145. package/dist/tools/uk-carbon.d.ts +51 -0
  146. package/dist/tools/uk-carbon.js +109 -0
  147. package/dist/tools/uk-grid.d.ts +28 -0
  148. package/dist/tools/uk-grid.js +70 -0
  149. package/dist/tools/us-gas.d.ts +30 -0
  150. package/dist/tools/us-gas.js +100 -0
  151. package/dist/tools/verify-gis-sources.d.ts +25 -0
  152. package/dist/tools/verify-gis-sources.js +119 -0
  153. package/dist/tools/weather.d.ts +27 -0
  154. package/dist/tools/weather.js +120 -0
  155. package/package.json +62 -0
@@ -0,0 +1,199 @@
1
+ import { z } from "zod";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { timingSafeEqual } from "node:crypto";
4
+ import { join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ // ConfigurationError
7
+ export class ConfigurationError extends Error {
8
+ constructor(keyName) {
9
+ super(`API key "${keyName}" is not configured.\n\n` +
10
+ `Resolution order:\n` +
11
+ ` 1. Environment variable: ${keyName}\n` +
12
+ ` 2. Key file: ${join(homedir(), ".luminus", "keys.json")} → { "${keyName}": "..." }\n\n` +
13
+ `Set one of the above and restart the server.`);
14
+ this.name = "ConfigurationError";
15
+ }
16
+ }
17
+ // Key file schema + cache
18
+ const KeyFileSchema = z.record(z.string(), z.string());
19
+ let cachedKeyFile = null;
20
+ let keyFileChecked = false;
21
+ const KEYS_PATH = join(homedir(), ".luminus", "keys.json");
22
+ async function warnOpenPermissions(filePath) {
23
+ if (process.platform === "win32")
24
+ return;
25
+ try {
26
+ const info = await stat(filePath);
27
+ const mode = info.mode & 0o777;
28
+ if (mode & 0o077) {
29
+ process.stderr.write(`[luminus] WARNING: ${filePath} is readable by others (mode ${mode.toString(8)}). ` +
30
+ `Run: chmod 600 ${filePath}\n`);
31
+ }
32
+ }
33
+ catch {
34
+ // stat failed — file may not exist, which is fine
35
+ }
36
+ }
37
+ async function loadKeyFile() {
38
+ if (keyFileChecked)
39
+ return cachedKeyFile ?? {};
40
+ keyFileChecked = true;
41
+ try {
42
+ await warnOpenPermissions(KEYS_PATH);
43
+ const raw = await readFile(KEYS_PATH, "utf-8");
44
+ const parsed = KeyFileSchema.parse(JSON.parse(raw));
45
+ cachedKeyFile = parsed;
46
+ return parsed;
47
+ }
48
+ catch (err) {
49
+ // Distinguish "file not found" (normal) from "file exists but is broken" (warn)
50
+ const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
51
+ if (!isNotFound) {
52
+ process.stderr.write(`[luminus] WARNING: failed to load ${KEYS_PATH}: ${err instanceof Error ? err.message : String(err)}\n`);
53
+ }
54
+ cachedKeyFile = null;
55
+ return {};
56
+ }
57
+ }
58
+ // Resolved key cache (in-memory, per-process)
59
+ const resolvedKeys = new Map();
60
+ /**
61
+ * Resolve an API key by name. Tries environment variables first, then
62
+ * ~/.luminus/keys.json. Throws ConfigurationError if not found.
63
+ * Resolved values are cached in memory for the process lifetime.
64
+ */
65
+ export async function resolveApiKey(name) {
66
+ const cached = resolvedKeys.get(name);
67
+ if (cached !== undefined)
68
+ return cached;
69
+ // Layer 1: environment variable
70
+ const envValue = process.env[name];
71
+ if (envValue) {
72
+ resolvedKeys.set(name, envValue);
73
+ return envValue;
74
+ }
75
+ // Layer 2: key file
76
+ const fileKeys = await loadKeyFile();
77
+ const fileValue = fileKeys[name];
78
+ if (fileValue) {
79
+ resolvedKeys.set(name, fileValue);
80
+ return fileValue;
81
+ }
82
+ // Not found
83
+ throw new ConfigurationError(name);
84
+ }
85
+ /** Constant-time string comparison. Reserved for future client authentication. */
86
+ export function timingSafeCompare(a, b) {
87
+ const bufA = Buffer.from(a);
88
+ const bufB = Buffer.from(b);
89
+ if (bufA.length !== bufB.length)
90
+ return false;
91
+ return timingSafeEqual(bufA, bufB);
92
+ }
93
+ /**
94
+ * Map of tool name to required API key names.
95
+ * Empty array = public API, no key needed.
96
+ */
97
+ export const TOOL_KEY_REQUIREMENTS = {
98
+ // ENTSO-E tools
99
+ get_generation_mix: ["ENTSOE_API_KEY"],
100
+ get_day_ahead_prices: ["ENTSOE_API_KEY"],
101
+ get_cross_border_flows: ["ENTSOE_API_KEY"],
102
+ get_carbon_intensity: ["ENTSOE_API_KEY"],
103
+ get_balancing_prices: ["ENTSOE_API_KEY"],
104
+ get_renewable_forecast: ["ENTSOE_API_KEY"],
105
+ get_demand_forecast: ["ENTSOE_API_KEY"],
106
+ get_outages: ["ENTSOE_API_KEY"],
107
+ get_net_positions: ["ENTSOE_API_KEY"],
108
+ get_transfer_capacities: ["ENTSOE_API_KEY"],
109
+ get_hydro_reservoir: ["ENTSOE_API_KEY"],
110
+ get_intraday_prices: ["ENTSOE_API_KEY"],
111
+ get_imbalance_prices: ["ENTSOE_API_KEY"],
112
+ get_intraday_da_spread: ["ENTSOE_API_KEY"],
113
+ get_realtime_generation: ["ENTSOE_API_KEY"],
114
+ get_balancing_actions: ["ENTSOE_API_KEY"],
115
+ get_ancillary_prices: ["ENTSOE_API_KEY"],
116
+ get_remit_messages: ["ENTSOE_API_KEY"],
117
+ get_price_spread_analysis: ["ENTSOE_API_KEY"],
118
+ // GIE (Gas Infrastructure Europe) tools
119
+ get_gas_storage: ["GIE_API_KEY"],
120
+ get_lng_terminals: ["GIE_API_KEY"],
121
+ // EIA (US Energy Information Administration)
122
+ get_us_gas_data: ["EIA_API_KEY"],
123
+ // Fingrid (Finnish grid)
124
+ get_fingrid_data: ["FINGRID_API_KEY"],
125
+ // REE ESIOS (Spanish grid)
126
+ get_ree_esios: ["ESIOS_API_TOKEN"],
127
+ // Storm Glass (marine weather)
128
+ get_stormglass: ["STORMGLASS_API_KEY"],
129
+ // Public APIs — no key required
130
+ get_weather_forecast: [],
131
+ get_uk_carbon_intensity: [],
132
+ get_uk_grid_demand: [],
133
+ get_power_plants: [],
134
+ get_auction_results: [],
135
+ get_solar_irradiance: [],
136
+ get_eu_frequency: [],
137
+ get_transmission_lines: [],
138
+ get_energy_charts: [],
139
+ get_commodity_prices: [],
140
+ get_nordpool_prices: [],
141
+ get_smard_data: [],
142
+ get_entsog_data: [],
143
+ get_elexon_bmrs: [],
144
+ get_era5_weather: [],
145
+ get_regelleistung: [],
146
+ get_rte_france: [],
147
+ get_energi_data: [],
148
+ get_hydro_inflows: [],
149
+ get_acer_remit: [],
150
+ get_terna_data: [],
151
+ get_eu_gas_price: [],
152
+ get_terrain_analysis: [],
153
+ get_grid_proximity: [],
154
+ get_grid_connection_queue: [],
155
+ get_grid_connection_intelligence: [],
156
+ get_land_constraints: [],
157
+ get_agricultural_land: [],
158
+ get_flood_risk: [],
159
+ screen_site: [],
160
+ verify_gis_sources: [],
161
+ compare_sites: [],
162
+ estimate_site_revenue: ["ENTSOE_API_KEY"],
163
+ };
164
+ /**
165
+ * Check whether all API keys for a tool are resolvable right now.
166
+ * Checks environment variables and the cached key file synchronously.
167
+ * Returns true for tools with no key requirements.
168
+ */
169
+ export function hasRequiredKeys(toolName) {
170
+ const requirements = TOOL_KEY_REQUIREMENTS[toolName];
171
+ if (!requirements || requirements.length === 0)
172
+ return true;
173
+ return requirements.every((keyName) => {
174
+ // Check in-memory cache first
175
+ if (resolvedKeys.has(keyName))
176
+ return true;
177
+ // Check env
178
+ if (process.env[keyName])
179
+ return true;
180
+ // Check file cache (loaded asynchronously, may not be ready yet)
181
+ if (cachedKeyFile?.[keyName])
182
+ return true;
183
+ return false;
184
+ });
185
+ }
186
+ /** Check whether a specific API key name is available (env or key file). */
187
+ export function isKeyConfigured(keyName) {
188
+ if (resolvedKeys.has(keyName))
189
+ return true;
190
+ if (process.env[keyName])
191
+ return true;
192
+ if (cachedKeyFile?.[keyName])
193
+ return true;
194
+ return false;
195
+ }
196
+ /** Pre-load keys.json so hasRequiredKeys works immediately. */
197
+ export async function preloadKeyFile() {
198
+ await loadKeyFile();
199
+ }
@@ -0,0 +1,25 @@
1
+ /** Simple in-memory TTL cache */
2
+ export declare class TtlCache {
3
+ private store;
4
+ get<T>(key: string): T | undefined;
5
+ set<T>(key: string, data: T, ttlMs: number): void;
6
+ clear(): void;
7
+ }
8
+ /** Cache TTL constants in milliseconds */
9
+ export declare const TTL: {
10
+ readonly REALTIME: number;
11
+ readonly PRICES: number;
12
+ readonly CAPACITY: number;
13
+ readonly FLOWS: number;
14
+ readonly STORAGE: number;
15
+ readonly WEATHER: number;
16
+ readonly EIA: number;
17
+ readonly FORECAST: number;
18
+ readonly BALANCING: number;
19
+ readonly STATIC_DATA: number;
20
+ readonly AUCTION: number;
21
+ readonly ANCILLARY: number;
22
+ readonly OUTAGES: number;
23
+ readonly FREQUENCY: number;
24
+ readonly INTRADAY: number;
25
+ };
@@ -0,0 +1,38 @@
1
+ /** Simple in-memory TTL cache */
2
+ export class TtlCache {
3
+ store = new Map();
4
+ get(key) {
5
+ const entry = this.store.get(key);
6
+ if (!entry)
7
+ return undefined;
8
+ if (Date.now() > entry.expiresAt) {
9
+ this.store.delete(key);
10
+ return undefined;
11
+ }
12
+ return entry.data;
13
+ }
14
+ set(key, data, ttlMs) {
15
+ this.store.set(key, { data, expiresAt: Date.now() + ttlMs });
16
+ }
17
+ clear() {
18
+ this.store.clear();
19
+ }
20
+ }
21
+ /** Cache TTL constants in milliseconds */
22
+ export const TTL = {
23
+ REALTIME: 5 * 60 * 1000, // 5 min
24
+ PRICES: 60 * 60 * 1000, // 1 hour
25
+ CAPACITY: 24 * 60 * 60 * 1000, // 24 hours
26
+ FLOWS: 5 * 60 * 1000, // 5 min
27
+ STORAGE: 60 * 60 * 1000, // 1 hour
28
+ WEATHER: 30 * 60 * 1000, // 30 min
29
+ EIA: 60 * 60 * 1000, // 1 hour
30
+ FORECAST: 60 * 60 * 1000, // 1 hour
31
+ BALANCING: 5 * 60 * 1000, // 5 min
32
+ STATIC_DATA: 24 * 60 * 60 * 1000, // 24 hours
33
+ AUCTION: 60 * 60 * 1000, // 1 hour
34
+ ANCILLARY: 60 * 60 * 1000, // 1 hour
35
+ OUTAGES: 15 * 60 * 1000, // 15 min
36
+ FREQUENCY: 30 * 1000, // 30 sec
37
+ INTRADAY: 15 * 60 * 1000, // 15 min
38
+ };
@@ -0,0 +1 @@
1
+ export declare function parseProfileArg(argv: string[]): string;
@@ -0,0 +1,10 @@
1
+ export function parseProfileArg(argv) {
2
+ const idx = argv.indexOf("--profile");
3
+ if (idx === -1)
4
+ return "full";
5
+ const value = argv[idx + 1];
6
+ if (!value || value.startsWith("--")) {
7
+ throw new Error("Missing value for --profile.");
8
+ }
9
+ return value;
10
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * CORINE Land Cover 2018 — EEA ArcGIS REST client.
3
+ *
4
+ * Data source: Copernicus Land Monitoring Service / EEA
5
+ * Licence: Copernicus (free, attribution required)
6
+ * Coverage: EU27 + EEA/EFTA. Great Britain is NOT covered (UK left the
7
+ * programme after CLC 2012). Return null for GB queries.
8
+ *
9
+ * CLC uses a three-level hierarchical nomenclature with 44 classes.
10
+ * We return the raw code (string, e.g. "211"), the human label, the
11
+ * top-level class group, and a conservative `is_planning_exclusion` flag.
12
+ */
13
+ /** EU member states + EEA/EFTA covered by CORINE 2018. */
14
+ export declare const CORINE_COVERED_COUNTRIES: Set<string>;
15
+ export interface CorineResult {
16
+ /** 3-digit CLC code string, e.g. "211" */
17
+ code: string;
18
+ /** Human-readable label, e.g. "Non-irrigated arable land" */
19
+ label: string;
20
+ /** Top-level CLC class group */
21
+ class_group: string;
22
+ /**
23
+ * True if this land-cover type typically faces strong planning barriers
24
+ * for PV/BESS development. Conservative: only wetlands, water bodies,
25
+ * and semi-natural woodland are flagged. Agricultural land is NOT flagged
26
+ * here — that risk is assessed separately by ALC/national tools.
27
+ */
28
+ is_planning_exclusion: boolean;
29
+ source: "corine-land-cover-2018";
30
+ }
31
+ export declare function queryCorineAtPoint(lat: number, lon: number): Promise<CorineResult | null>;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * CORINE Land Cover 2018 — EEA ArcGIS REST client.
3
+ *
4
+ * Data source: Copernicus Land Monitoring Service / EEA
5
+ * Licence: Copernicus (free, attribution required)
6
+ * Coverage: EU27 + EEA/EFTA. Great Britain is NOT covered (UK left the
7
+ * programme after CLC 2012). Return null for GB queries.
8
+ *
9
+ * CLC uses a three-level hierarchical nomenclature with 44 classes.
10
+ * We return the raw code (string, e.g. "211"), the human label, the
11
+ * top-level class group, and a conservative `is_planning_exclusion` flag.
12
+ */
13
+ import { guardArcGisFields } from "./schema-guard.js";
14
+ const CORINE_QUERY_URL = "https://image.discomap.eea.europa.eu/arcgis/rest/services/Corine/CLC2018_WM/MapServer/0/query";
15
+ /** EU member states + EEA/EFTA covered by CORINE 2018. */
16
+ export const CORINE_COVERED_COUNTRIES = new Set([
17
+ "AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
18
+ "FR", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT",
19
+ "NL", "PL", "PT", "RO", "SE", "SI", "SK",
20
+ // EEA/EFTA (also in CORINE)
21
+ "IS", "LI", "NO", "TR", "AL", "BA", "ME", "MK", "RS", "XK",
22
+ ]);
23
+ /** Subset of the 44-class CLC 2018 nomenclature.
24
+ * Codes not in this map fall back to group-level labelling.
25
+ */
26
+ const CLC_CODES = {
27
+ // 1xx — Artificial surfaces
28
+ "111": { label: "Continuous urban fabric", group: "Artificial surfaces", exclusion: false },
29
+ "112": { label: "Discontinuous urban fabric", group: "Artificial surfaces", exclusion: false },
30
+ "121": { label: "Industrial or commercial units", group: "Artificial surfaces", exclusion: false },
31
+ "122": { label: "Road and rail networks and associated land", group: "Artificial surfaces", exclusion: false },
32
+ "123": { label: "Port areas", group: "Artificial surfaces", exclusion: false },
33
+ "124": { label: "Airports", group: "Artificial surfaces", exclusion: false },
34
+ "131": { label: "Mineral extraction sites", group: "Artificial surfaces", exclusion: false },
35
+ "132": { label: "Dump sites", group: "Artificial surfaces", exclusion: false },
36
+ "133": { label: "Construction sites", group: "Artificial surfaces", exclusion: false },
37
+ "141": { label: "Green urban areas", group: "Artificial surfaces", exclusion: false },
38
+ "142": { label: "Sport and leisure facilities", group: "Artificial surfaces", exclusion: false },
39
+ // 2xx — Agricultural areas
40
+ "211": { label: "Non-irrigated arable land", group: "Agricultural areas", exclusion: false },
41
+ "212": { label: "Permanently irrigated land", group: "Agricultural areas", exclusion: false },
42
+ "213": { label: "Rice fields", group: "Agricultural areas", exclusion: false },
43
+ "221": { label: "Vineyards", group: "Agricultural areas", exclusion: false },
44
+ "222": { label: "Fruit trees and berry plantations", group: "Agricultural areas", exclusion: false },
45
+ "223": { label: "Olive groves", group: "Agricultural areas", exclusion: false },
46
+ "231": { label: "Pastures", group: "Agricultural areas", exclusion: false },
47
+ "241": { label: "Annual crops associated with permanent crops", group: "Agricultural areas", exclusion: false },
48
+ "242": { label: "Complex cultivation patterns", group: "Agricultural areas", exclusion: false },
49
+ "243": { label: "Land principally occupied by agriculture with significant areas of natural vegetation", group: "Agricultural areas", exclusion: false },
50
+ "244": { label: "Agro-forestry areas", group: "Agricultural areas", exclusion: false },
51
+ // 3xx — Forest and semi-natural areas
52
+ "311": { label: "Broad-leaved forest", group: "Forest and semi-natural areas", exclusion: true },
53
+ "312": { label: "Coniferous forest", group: "Forest and semi-natural areas", exclusion: true },
54
+ "313": { label: "Mixed forest", group: "Forest and semi-natural areas", exclusion: true },
55
+ "321": { label: "Natural grasslands", group: "Forest and semi-natural areas", exclusion: false },
56
+ "322": { label: "Moors and heathland", group: "Forest and semi-natural areas", exclusion: false },
57
+ "323": { label: "Sclerophyllous vegetation", group: "Forest and semi-natural areas", exclusion: false },
58
+ "324": { label: "Transitional woodland-shrub", group: "Forest and semi-natural areas", exclusion: false },
59
+ "331": { label: "Beaches, dunes, sands", group: "Forest and semi-natural areas", exclusion: false },
60
+ "332": { label: "Bare rocks", group: "Forest and semi-natural areas", exclusion: false },
61
+ "333": { label: "Sparsely vegetated areas", group: "Forest and semi-natural areas", exclusion: false },
62
+ "334": { label: "Burnt areas", group: "Forest and semi-natural areas", exclusion: false },
63
+ "335": { label: "Glaciers and perpetual snow", group: "Forest and semi-natural areas", exclusion: false },
64
+ // 4xx — Wetlands
65
+ "411": { label: "Inland marshes", group: "Wetlands", exclusion: true },
66
+ "412": { label: "Peat bogs", group: "Wetlands", exclusion: true },
67
+ "421": { label: "Salt marshes", group: "Wetlands", exclusion: true },
68
+ "422": { label: "Salines", group: "Wetlands", exclusion: true },
69
+ "423": { label: "Intertidal flats", group: "Wetlands", exclusion: true },
70
+ // 5xx — Water bodies
71
+ "511": { label: "Water courses", group: "Water bodies", exclusion: true },
72
+ "512": { label: "Water bodies", group: "Water bodies", exclusion: true },
73
+ "521": { label: "Coastal lagoons", group: "Water bodies", exclusion: true },
74
+ "522": { label: "Estuaries", group: "Water bodies", exclusion: true },
75
+ "523": { label: "Sea and ocean", group: "Water bodies", exclusion: true },
76
+ };
77
+ function getGroupFromCode(code) {
78
+ const first = code.charAt(0);
79
+ switch (first) {
80
+ case "1": return "Artificial surfaces";
81
+ case "2": return "Agricultural areas";
82
+ case "3": return "Forest and semi-natural areas";
83
+ case "4": return "Wetlands";
84
+ case "5": return "Water bodies";
85
+ default: return "Unknown";
86
+ }
87
+ }
88
+ function mapClcCode(code) {
89
+ const entry = CLC_CODES[code];
90
+ if (entry)
91
+ return { label: entry.label, group: entry.group, exclusion: entry.exclusion };
92
+ const group = getGroupFromCode(code);
93
+ return { label: `CLC code ${code}`, group, exclusion: false };
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // API client
97
+ // ---------------------------------------------------------------------------
98
+ export async function queryCorineAtPoint(lat, lon) {
99
+ const url = new URL(CORINE_QUERY_URL);
100
+ const p = url.searchParams;
101
+ p.set("geometry", `${lon},${lat}`);
102
+ p.set("geometryType", "esriGeometryPoint");
103
+ p.set("inSR", "4326");
104
+ p.set("spatialRel", "esriSpatialRelIntersects");
105
+ p.set("outFields", "Code_18");
106
+ p.set("returnGeometry", "false");
107
+ p.set("resultRecordCount", "1");
108
+ p.set("f", "json");
109
+ const response = await fetch(url.toString());
110
+ if (!response.ok) {
111
+ const body = await response.text();
112
+ throw new Error(`CORINE API returned ${response.status}: ${body.slice(0, 300)}`);
113
+ }
114
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
+ const json = await response.json();
116
+ if (json.error) {
117
+ throw new Error(`CORINE API error: ${json.error.message ?? JSON.stringify(json.error)}`);
118
+ }
119
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
120
+ const features = json.features ?? [];
121
+ if (features.length === 0) {
122
+ // No polygon hit — typically offshore, or at the very edge of coverage.
123
+ return null;
124
+ }
125
+ guardArcGisFields(features, ["Code_18"], "CORINE Land Cover");
126
+ const code = String(features[0].attributes?.Code_18 ?? "").trim();
127
+ if (!code)
128
+ return null;
129
+ const { label, group, exclusion } = mapClcCode(code);
130
+ return {
131
+ code,
132
+ label,
133
+ class_group: group,
134
+ is_planning_exclusion: exclusion,
135
+ source: "corine-land-cover-2018",
136
+ };
137
+ }
@@ -0,0 +1,7 @@
1
+ export interface EeaConstraintFeature {
2
+ name: string;
3
+ type: string;
4
+ area_ha: number | null;
5
+ source: string;
6
+ }
7
+ export declare function queryNatura2000Layer(lat: number, lon: number, radiusKm: number): Promise<EeaConstraintFeature[]>;
@@ -0,0 +1,53 @@
1
+ import { guardArcGisFields } from "./schema-guard.js";
2
+ const EEA_NATURA2000_BASE = "https://bio.discomap.eea.europa.eu/arcgis/rest/services/ProtectedSites/Natura2000_Dyna_WM/MapServer/0/query";
3
+ const BIRDS_DIRECTIVE_CODES = new Set(["A", "C", "D", "F", "H", "J"]);
4
+ const HABITATS_DIRECTIVE_CODES = new Set(["B", "E", "G", "I", "K", "c"]);
5
+ function buildEnvelopeGeometry(lat, lon, radiusKm) {
6
+ const latDelta = radiusKm / 111.32;
7
+ const lonDelta = radiusKm / (111.32 * Math.cos(lat * (Math.PI / 180)));
8
+ return `${lon - lonDelta},${lat - latDelta},${lon + lonDelta},${lat + latDelta}`;
9
+ }
10
+ function mapSiteType(siteType) {
11
+ const value = String(siteType ?? "").trim();
12
+ if (BIRDS_DIRECTIVE_CODES.has(value))
13
+ return "natura2000_birds";
14
+ if (HABITATS_DIRECTIVE_CODES.has(value))
15
+ return "natura2000_habitats";
16
+ return "natura2000";
17
+ }
18
+ export async function queryNatura2000Layer(lat, lon, radiusKm) {
19
+ const url = new URL(EEA_NATURA2000_BASE);
20
+ const p = url.searchParams;
21
+ p.set("where", "1=1");
22
+ p.set("geometry", buildEnvelopeGeometry(lat, lon, radiusKm));
23
+ p.set("geometryType", "esriGeometryEnvelope");
24
+ p.set("inSR", "4326");
25
+ p.set("spatialRel", "esriSpatialRelIntersects");
26
+ p.set("outFields", "SITECODE,SITENAME,SITETYPE,MS,Area_km2");
27
+ p.set("returnGeometry", "false");
28
+ p.set("resultRecordCount", "50");
29
+ p.set("f", "json");
30
+ const response = await fetch(url.toString());
31
+ if (!response.ok) {
32
+ const body = await response.text();
33
+ throw new Error(`EEA Natura 2000 API returned ${response.status}: ${body.slice(0, 300)}`);
34
+ }
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const json = await response.json();
37
+ if (json.error) {
38
+ throw new Error(`EEA Natura 2000 API error: ${json.error.message ?? JSON.stringify(json.error)}`);
39
+ }
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ const features = json.features ?? [];
42
+ guardArcGisFields(features, ["SITECODE", "SITENAME", "SITETYPE", "MS", "Area_km2"], "EEA Natura 2000");
43
+ return features.map((feature) => {
44
+ const attrs = feature.attributes ?? {};
45
+ const areaKm2 = attrs.Area_km2;
46
+ return {
47
+ name: String(attrs.SITENAME ?? attrs.SITECODE ?? "Unknown Natura 2000 site"),
48
+ type: mapSiteType(attrs.SITETYPE),
49
+ area_ha: typeof areaKm2 === "number" ? Math.round(areaKm2 * 100 * 100) / 100 : null,
50
+ source: "eea-natura2000",
51
+ };
52
+ });
53
+ }
@@ -0,0 +1,22 @@
1
+ export interface EntsoeParams {
2
+ documentType: string;
3
+ processType?: string;
4
+ in_Domain?: string;
5
+ out_Domain?: string;
6
+ periodStart: string;
7
+ periodEnd: string;
8
+ [key: string]: string | undefined;
9
+ }
10
+ /** Format a Date as ENTSO-E expects: YYYYMMDDHHmm (UTC) */
11
+ export declare function formatEntsoeDate(date: Date): string;
12
+ /** Build start/end timestamps for a single day query */
13
+ export declare function dayRange(dateStr?: string): {
14
+ periodStart: string;
15
+ periodEnd: string;
16
+ };
17
+ /**
18
+ * Query the ENTSO-E API.
19
+ * Returns parsed XML as JS object.
20
+ * Caches by URL with the given TTL.
21
+ */
22
+ export declare function queryEntsoe(params: EntsoeParams, ttlMs?: number): Promise<Record<string, unknown>>;
@@ -0,0 +1,69 @@
1
+ import { parseXml } from "./xml-parser.js";
2
+ import { TtlCache, TTL } from "./cache.js";
3
+ import { resolveApiKey } from "./auth.js";
4
+ const BASE_URL = "https://web-api.tp.entsoe.eu/api";
5
+ const cache = new TtlCache();
6
+ async function getApiKey() {
7
+ try {
8
+ return await resolveApiKey("ENTSOE_API_KEY");
9
+ }
10
+ catch {
11
+ throw new Error("ENTSOE_API_KEY is required. Set it as an environment variable or in ~/.luminus/keys.json. " +
12
+ "Get one at https://transparency.entsoe.eu/ (register → email token).");
13
+ }
14
+ }
15
+ /** Format a Date as ENTSO-E expects: YYYYMMDDHHmm (UTC) */
16
+ export function formatEntsoeDate(date) {
17
+ const y = date.getUTCFullYear();
18
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
19
+ const d = String(date.getUTCDate()).padStart(2, "0");
20
+ const h = String(date.getUTCHours()).padStart(2, "0");
21
+ const min = String(date.getUTCMinutes()).padStart(2, "0");
22
+ return `${y}${m}${d}${h}${min}`;
23
+ }
24
+ /** Build start/end timestamps for a single day query */
25
+ export function dayRange(dateStr) {
26
+ const base = dateStr ? new Date(dateStr + "T00:00:00Z") : new Date();
27
+ const start = new Date(Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate()));
28
+ const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
29
+ return {
30
+ periodStart: formatEntsoeDate(start),
31
+ periodEnd: formatEntsoeDate(end),
32
+ };
33
+ }
34
+ /**
35
+ * Query the ENTSO-E API.
36
+ * Returns parsed XML as JS object.
37
+ * Caches by URL with the given TTL.
38
+ */
39
+ export async function queryEntsoe(params, ttlMs = TTL.REALTIME) {
40
+ const url = new URL(BASE_URL);
41
+ url.searchParams.set("securityToken", await getApiKey());
42
+ for (const [key, value] of Object.entries(params)) {
43
+ if (value != null) {
44
+ url.searchParams.set(key, value);
45
+ }
46
+ }
47
+ const cacheKey = url.toString().replace(/securityToken=[^&]+/, "token=***");
48
+ const cached = cache.get(cacheKey);
49
+ if (cached)
50
+ return cached;
51
+ const response = await fetch(url.toString());
52
+ if (!response.ok) {
53
+ const body = await response.text();
54
+ // ENTSO-E returns error details in XML
55
+ if (body.includes("Reason")) {
56
+ const parsed = parseXml(body);
57
+ const reason =
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ parsed?.Acknowledgement_MarketDocument?.Reason?.text ??
60
+ body.slice(0, 300);
61
+ throw new Error(`ENTSO-E API error: ${reason}`);
62
+ }
63
+ throw new Error(`ENTSO-E API returned ${response.status}: ${body.slice(0, 300)}`);
64
+ }
65
+ const xml = await response.text();
66
+ const result = parseXml(xml);
67
+ cache.set(cacheKey, result, ttlMs);
68
+ return result;
69
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * GIS data source metadata — provenance, reliability, and caveats.
3
+ *
4
+ * Each GIS tool response includes a `source_metadata` block drawn from
5
+ * these definitions. The goal is to make data quality and upstream
6
+ * limitations visible to callers, not hidden behind a clean API surface.
7
+ */
8
+ export interface GisSourceMetadata {
9
+ readonly id: string;
10
+ readonly name: string;
11
+ readonly provider: string;
12
+ readonly licence: string;
13
+ readonly url: string;
14
+ readonly api_key_required: boolean;
15
+ readonly coverage: string;
16
+ readonly update_frequency: string;
17
+ readonly reliability: "high" | "medium" | "low";
18
+ readonly caveats: readonly string[];
19
+ readonly attribution: string;
20
+ readonly verified_at?: string;
21
+ }
22
+ export declare const GIS_SOURCES: Readonly<Record<string, GisSourceMetadata>>;
23
+ /** Health check endpoint for each GIS source. */
24
+ export interface GisHealthCheckConfig {
25
+ readonly source_id: string;
26
+ readonly url: string;
27
+ readonly method: "GET" | "POST";
28
+ readonly body?: string;
29
+ readonly timeout_ms: number;
30
+ /** A function that checks whether the response body looks sane. */
31
+ readonly validate: (status: number, body: string) => string | null;
32
+ }
33
+ export declare const GIS_HEALTH_CHECKS: readonly GisHealthCheckConfig[];