@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,109 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ const CARBON_API = "https://api.carbonintensity.org.uk";
4
+ const cache = new TtlCache();
5
+ export const ukCarbonSchema = z.object({
6
+ action: z
7
+ .enum(["current", "regional", "date"])
8
+ .describe('"current" = national carbon intensity + generation mix. ' +
9
+ '"regional" = carbon intensity by GB region. ' +
10
+ '"date" = historical carbon intensity for a specific date (YYYY-MM-DD).'),
11
+ date: z
12
+ .string()
13
+ .optional()
14
+ .describe('Date in YYYY-MM-DD format. Required for action "date".'),
15
+ });
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ async function fetchCarbon(path) {
18
+ const url = `${CARBON_API}${path}`;
19
+ const cached = cache.get(url);
20
+ if (cached)
21
+ return cached;
22
+ const response = await fetch(url);
23
+ if (!response.ok) {
24
+ const body = await response.text();
25
+ throw new Error(`Carbon Intensity API returned ${response.status}: ${body.slice(0, 300)}`);
26
+ }
27
+ const json = await response.json();
28
+ cache.set(url, json, TTL.REALTIME);
29
+ return json;
30
+ }
31
+ async function getCurrent() {
32
+ const [intensityData, genData] = await Promise.all([
33
+ fetchCarbon("/intensity"),
34
+ fetchCarbon("/generation"),
35
+ ]);
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const intensity = intensityData?.data?.[0] ?? {};
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const genMix = genData?.data?.generationmix ?? [];
40
+ return {
41
+ action: "current",
42
+ timestamp: intensity.from ?? new Date().toISOString(),
43
+ intensity_gco2_kwh: Number(intensity.intensity?.actual ?? intensity.intensity?.forecast ?? 0),
44
+ index: intensity.intensity?.index ?? "unknown",
45
+ generation_mix: genMix.map((g) => ({
46
+ fuel: g.fuel ?? "",
47
+ perc: Number(g.perc ?? 0),
48
+ })),
49
+ };
50
+ }
51
+ async function getRegional() {
52
+ const data = await fetchCarbon("/regional");
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ const regions = data?.data?.[0]?.regions ?? [];
55
+ return {
56
+ action: "regional",
57
+ timestamp: data?.data?.[0]?.from ?? new Date().toISOString(),
58
+ regions: regions.map((r) => ({
59
+ region: r.shortname ?? r.dnoregion ?? "unknown",
60
+ intensity_gco2_kwh: Number(r.intensity?.forecast ?? 0),
61
+ index: r.intensity?.index ?? "unknown",
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ generation_mix: (r.generationmix ?? []).map((g) => ({
64
+ fuel: g.fuel ?? "",
65
+ perc: Number(g.perc ?? 0),
66
+ })),
67
+ })),
68
+ };
69
+ }
70
+ async function getByDate(date) {
71
+ const data = await fetchCarbon(`/intensity/date/${date}`);
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ const periods = data?.data ?? [];
74
+ const entries = periods.map((p) => ({
75
+ from: p.from ?? "",
76
+ to: p.to ?? "",
77
+ intensity_forecast: Number(p.intensity?.forecast ?? 0),
78
+ intensity_actual: p.intensity?.actual != null ? Number(p.intensity.actual) : null,
79
+ index: p.intensity?.index ?? "unknown",
80
+ }));
81
+ const forecasts = entries.map((e) => e.intensity_forecast).filter((v) => v > 0);
82
+ const mean = forecasts.length > 0
83
+ ? Math.round((forecasts.reduce((s, v) => s + v, 0) / forecasts.length) * 10) / 10
84
+ : 0;
85
+ return {
86
+ action: "date",
87
+ date,
88
+ periods: entries,
89
+ stats: {
90
+ mean_forecast: mean,
91
+ min_forecast: forecasts.length > 0 ? Math.min(...forecasts) : 0,
92
+ max_forecast: forecasts.length > 0 ? Math.max(...forecasts) : 0,
93
+ },
94
+ };
95
+ }
96
+ export async function getUkCarbonIntensity(params) {
97
+ switch (params.action) {
98
+ case "current":
99
+ return getCurrent();
100
+ case "regional":
101
+ return getRegional();
102
+ case "date": {
103
+ if (!params.date) {
104
+ throw new Error('Date parameter is required for action "date".');
105
+ }
106
+ return getByDate(params.date);
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ export declare const ukGridSchema: z.ZodObject<{
3
+ action: z.ZodEnum<{
4
+ frequency: "frequency";
5
+ demand: "demand";
6
+ }>;
7
+ }, z.core.$strip>;
8
+ interface DemandRecord {
9
+ timestamp: string;
10
+ settlement_period: number;
11
+ demand_mw: number;
12
+ transmission_demand_mw: number;
13
+ }
14
+ interface DemandResult {
15
+ action: "demand";
16
+ description: string;
17
+ records: DemandRecord[];
18
+ }
19
+ interface FrequencyResult {
20
+ action: "frequency";
21
+ description: string;
22
+ timestamp: string;
23
+ frequency_hz: number;
24
+ deviation_hz: number;
25
+ }
26
+ type UkGridResult = DemandResult | FrequencyResult;
27
+ export declare function getUkGridDemand(params: z.infer<typeof ukGridSchema>): Promise<UkGridResult>;
28
+ export {};
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ const ELEXON_API = "https://data.elexon.co.uk/bmrs/api/v1";
4
+ const cache = new TtlCache();
5
+ export const ukGridSchema = z.object({
6
+ action: z
7
+ .enum(["demand", "frequency"])
8
+ .describe('"demand" = current GB demand actuals (MW) per settlement period. ' +
9
+ '"frequency" = real-time grid frequency (~50 Hz; deviations indicate stress).'),
10
+ });
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ async function fetchElexon(path) {
13
+ const url = `${ELEXON_API}${path}`;
14
+ const cached = cache.get(url);
15
+ if (cached)
16
+ return cached;
17
+ const response = await fetch(url);
18
+ if (!response.ok) {
19
+ const body = await response.text();
20
+ throw new Error(`Elexon BMRS API returned ${response.status}: ${body.slice(0, 300)}`);
21
+ }
22
+ const json = await response.json();
23
+ cache.set(url, json, TTL.REALTIME);
24
+ return json;
25
+ }
26
+ async function getDemand() {
27
+ const today = new Date().toISOString().slice(0, 10);
28
+ const data = await fetchElexon(`/demand/outturn?settlementDateFrom=${today}&settlementDateTo=${today}&format=json`);
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ const rows = data?.data ?? [];
31
+ // Take last 24 periods (12 hours) for a useful window
32
+ const recent = rows.slice(-24);
33
+ const records = recent.map((r) => ({
34
+ timestamp: r.startTime ?? "",
35
+ settlement_period: Number(r.settlementPeriod ?? 0),
36
+ demand_mw: Number(r.initialDemandOutturn ?? 0),
37
+ transmission_demand_mw: Number(r.initialTransmissionSystemDemandOutturn ?? 0),
38
+ }));
39
+ return {
40
+ action: "demand",
41
+ description: "GB electricity demand outturn (MW), recent settlement periods",
42
+ records,
43
+ };
44
+ }
45
+ async function getFrequency() {
46
+ const data = await fetchElexon("/datasets/FREQ?format=json");
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ const rows = data?.data ?? [];
49
+ if (rows.length === 0) {
50
+ throw new Error("No frequency data available from Elexon BMRS.");
51
+ }
52
+ // First record is the most recent
53
+ const row = rows[0];
54
+ const freq = Number(row.frequency ?? 50.0);
55
+ return {
56
+ action: "frequency",
57
+ description: "GB grid frequency (Hz). Nominal 50 Hz; deviations indicate grid stress.",
58
+ timestamp: row.measurementTime ?? new Date().toISOString(),
59
+ frequency_hz: Math.round(freq * 1000) / 1000,
60
+ deviation_hz: Math.round((freq - 50.0) * 1000) / 1000,
61
+ };
62
+ }
63
+ export async function getUkGridDemand(params) {
64
+ switch (params.action) {
65
+ case "demand":
66
+ return getDemand();
67
+ case "frequency":
68
+ return getFrequency();
69
+ }
70
+ }
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ export declare const usGasSchema: z.ZodObject<{
3
+ dataset: z.ZodEnum<{
4
+ storage: "storage";
5
+ henry_hub: "henry_hub";
6
+ }>;
7
+ limit: z.ZodOptional<z.ZodNumber>;
8
+ }, z.core.$strip>;
9
+ interface StorageRecord {
10
+ period: string;
11
+ value_bcf: number;
12
+ region: string;
13
+ }
14
+ interface HenryHubRecord {
15
+ period: string;
16
+ price_usd_mmbtu: number;
17
+ }
18
+ interface UsGasStorageResult {
19
+ dataset: "storage";
20
+ description: string;
21
+ records: StorageRecord[];
22
+ }
23
+ interface UsGasHenryHubResult {
24
+ dataset: "henry_hub";
25
+ description: string;
26
+ records: HenryHubRecord[];
27
+ }
28
+ type UsGasResult = UsGasStorageResult | UsGasHenryHubResult;
29
+ export declare function getUsGasData(params: z.infer<typeof usGasSchema>): Promise<UsGasResult>;
30
+ export {};
@@ -0,0 +1,100 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ import { resolveApiKey } from "../lib/auth.js";
4
+ const BASE_URL = "https://api.eia.gov/v2";
5
+ const cache = new TtlCache();
6
+ export const usGasSchema = z.object({
7
+ dataset: z
8
+ .enum(["storage", "henry_hub"])
9
+ .describe('Dataset to query. "storage" = weekly US gas storage levels (Lower 48). ' +
10
+ '"henry_hub" = Henry Hub natural gas spot price.'),
11
+ limit: z
12
+ .number()
13
+ .optional()
14
+ .describe("Number of records to return. Defaults to 10."),
15
+ });
16
+ async function getApiKey() {
17
+ try {
18
+ return await resolveApiKey("EIA_API_KEY");
19
+ }
20
+ catch {
21
+ throw new Error("EIA_API_KEY is required. Set it as an environment variable or in ~/.luminus/keys.json. " +
22
+ "Get one at https://www.eia.gov/opendata/register.php");
23
+ }
24
+ }
25
+ async function fetchEia(path, params, ttlMs
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ ) {
28
+ const url = new URL(`${BASE_URL}${path}`);
29
+ url.searchParams.set("api_key", await getApiKey());
30
+ for (const [key, value] of Object.entries(params)) {
31
+ url.searchParams.set(key, value);
32
+ }
33
+ const cacheKey = url.toString().replace(/api_key=[^&]+/, "api_key=***");
34
+ const cached = cache.get(cacheKey);
35
+ if (cached)
36
+ return cached;
37
+ const response = await fetch(url.toString());
38
+ if (!response.ok) {
39
+ const body = await response.text();
40
+ throw new Error(`EIA API returned ${response.status}: ${body.slice(0, 300)}`);
41
+ }
42
+ const json = await response.json();
43
+ cache.set(cacheKey, json, ttlMs);
44
+ return json;
45
+ }
46
+ async function getWeeklyStorage(limit) {
47
+ // EIA v2 requires /data/ suffix for data queries
48
+ // Filter: R48 = Lower 48 total, SWO = Working Gas
49
+ const data = await fetchEia("/natural-gas/stor/wkly/data/", {
50
+ "data[]": "value",
51
+ "facets[duoarea][]": "R48",
52
+ "facets[process][]": "SWO",
53
+ frequency: "weekly",
54
+ "sort[0][column]": "period",
55
+ "sort[0][direction]": "desc",
56
+ length: String(limit),
57
+ }, TTL.EIA);
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ const rows = data?.response?.data ?? [];
60
+ const records = rows.map((r) => ({
61
+ period: r.period ?? "",
62
+ value_bcf: Number(r.value ?? 0),
63
+ region: r["series-description"] ?? "Lower 48",
64
+ }));
65
+ return {
66
+ dataset: "storage",
67
+ description: "US weekly natural gas working storage, Lower 48 (Bcf)",
68
+ records,
69
+ };
70
+ }
71
+ async function getHenryHub(limit) {
72
+ const data = await fetchEia("/natural-gas/pri/fut/data/", {
73
+ "data[]": "value",
74
+ "facets[series][]": "RNGWHHD",
75
+ frequency: "daily",
76
+ "sort[0][column]": "period",
77
+ "sort[0][direction]": "desc",
78
+ length: String(limit),
79
+ }, TTL.EIA);
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ const rows = data?.response?.data ?? [];
82
+ const records = rows.map((r) => ({
83
+ period: r.period ?? "",
84
+ price_usd_mmbtu: Number(r.value ?? 0),
85
+ }));
86
+ return {
87
+ dataset: "henry_hub",
88
+ description: "Henry Hub natural gas spot price (USD/MMBtu)",
89
+ records,
90
+ };
91
+ }
92
+ export async function getUsGasData(params) {
93
+ const limit = params.limit ?? 10;
94
+ switch (params.dataset) {
95
+ case "storage":
96
+ return getWeeklyStorage(limit);
97
+ case "henry_hub":
98
+ return getHenryHub(limit);
99
+ }
100
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { type GisSourceMetadata } from "../lib/gis-sources.js";
3
+ export declare const verifyGisSourcesSchema: z.ZodObject<{
4
+ source_id: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$strip>;
6
+ interface SourceCheckResult {
7
+ source_id: string;
8
+ name: string;
9
+ status: "ok" | "degraded" | "unreachable";
10
+ response_time_ms: number | null;
11
+ error: string | null;
12
+ metadata: GisSourceMetadata;
13
+ }
14
+ interface VerifyGisSourcesResult {
15
+ checked_at: string;
16
+ sources: SourceCheckResult[];
17
+ summary: {
18
+ total: number;
19
+ ok: number;
20
+ degraded: number;
21
+ unreachable: number;
22
+ };
23
+ }
24
+ export declare function verifyGisSources(params: z.infer<typeof verifyGisSourcesSchema>): Promise<VerifyGisSourcesResult>;
25
+ export {};
@@ -0,0 +1,119 @@
1
+ import { z } from "zod";
2
+ import { GIS_SOURCES, GIS_HEALTH_CHECKS, } from "../lib/gis-sources.js";
3
+ export const verifyGisSourcesSchema = z.object({
4
+ source_id: z
5
+ .string()
6
+ .optional()
7
+ .describe('Check a single source by ID (e.g. "natural-england"). Omit to check all GIS sources.'),
8
+ });
9
+ async function checkSource(sourceId) {
10
+ const metadata = GIS_SOURCES[sourceId];
11
+ if (!metadata) {
12
+ return {
13
+ source_id: sourceId,
14
+ name: "Unknown",
15
+ status: "unreachable",
16
+ response_time_ms: null,
17
+ error: `No metadata defined for source "${sourceId}"`,
18
+ metadata: {},
19
+ };
20
+ }
21
+ const healthCheck = GIS_HEALTH_CHECKS.find((h) => h.source_id === sourceId);
22
+ if (!healthCheck) {
23
+ return {
24
+ source_id: sourceId,
25
+ name: metadata.name,
26
+ status: "degraded",
27
+ response_time_ms: null,
28
+ error: "No health check configured for this source",
29
+ metadata,
30
+ };
31
+ }
32
+ const start = Date.now();
33
+ try {
34
+ const controller = new AbortController();
35
+ const timeout = setTimeout(() => controller.abort(), healthCheck.timeout_ms);
36
+ const fetchOptions = {
37
+ method: healthCheck.method,
38
+ signal: controller.signal,
39
+ };
40
+ if (healthCheck.method === "POST" && healthCheck.body) {
41
+ fetchOptions.headers = {
42
+ "Content-Type": "application/x-www-form-urlencoded",
43
+ };
44
+ fetchOptions.body = healthCheck.body;
45
+ }
46
+ const response = await fetch(healthCheck.url, fetchOptions);
47
+ clearTimeout(timeout);
48
+ const elapsed = Date.now() - start;
49
+ const body = await response.text();
50
+ const validationError = healthCheck.validate(response.status, body);
51
+ if (validationError) {
52
+ return {
53
+ source_id: sourceId,
54
+ name: metadata.name,
55
+ status: "degraded",
56
+ response_time_ms: elapsed,
57
+ error: validationError,
58
+ metadata,
59
+ };
60
+ }
61
+ return {
62
+ source_id: sourceId,
63
+ name: metadata.name,
64
+ status: "ok",
65
+ response_time_ms: elapsed,
66
+ error: null,
67
+ metadata,
68
+ };
69
+ }
70
+ catch (err) {
71
+ const elapsed = Date.now() - start;
72
+ const message = err instanceof Error ? err.message : String(err);
73
+ const isTimeout = message.includes("abort");
74
+ return {
75
+ source_id: sourceId,
76
+ name: metadata.name,
77
+ status: "unreachable",
78
+ response_time_ms: isTimeout ? null : elapsed,
79
+ error: isTimeout
80
+ ? `Timed out after ${healthCheck.timeout_ms}ms`
81
+ : message,
82
+ metadata,
83
+ };
84
+ }
85
+ }
86
+ export async function verifyGisSources(params) {
87
+ const sourceIds = params.source_id
88
+ ? [params.source_id]
89
+ : Object.keys(GIS_SOURCES);
90
+ if (params.source_id && !GIS_SOURCES[params.source_id]) {
91
+ throw new Error(`Unknown source ID "${params.source_id}". Valid IDs: ${Object.keys(GIS_SOURCES).join(", ")}`);
92
+ }
93
+ const results = await Promise.allSettled(sourceIds.map((id) => checkSource(id)));
94
+ const sources = results.map((r, i) => {
95
+ if (r.status === "fulfilled")
96
+ return r.value;
97
+ return {
98
+ source_id: sourceIds[i],
99
+ name: GIS_SOURCES[sourceIds[i]]?.name ?? "Unknown",
100
+ status: "unreachable",
101
+ response_time_ms: null,
102
+ error: r.reason instanceof Error ? r.reason.message : String(r.reason),
103
+ metadata: GIS_SOURCES[sourceIds[i]] ?? {},
104
+ };
105
+ });
106
+ const ok = sources.filter((s) => s.status === "ok").length;
107
+ const degraded = sources.filter((s) => s.status === "degraded").length;
108
+ const unreachable = sources.filter((s) => s.status === "unreachable").length;
109
+ return {
110
+ checked_at: new Date().toISOString(),
111
+ sources,
112
+ summary: {
113
+ total: sources.length,
114
+ ok,
115
+ degraded,
116
+ unreachable,
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ export declare const weatherSchema: z.ZodObject<{
3
+ country: z.ZodOptional<z.ZodString>;
4
+ latitude: z.ZodOptional<z.ZodNumber>;
5
+ longitude: z.ZodOptional<z.ZodNumber>;
6
+ }, z.core.$strip>;
7
+ interface HourlyPoint {
8
+ time: string;
9
+ temperature_c: number;
10
+ wind_speed_kmh: number;
11
+ solar_radiation_wm2: number;
12
+ }
13
+ interface WeatherResult {
14
+ location: string;
15
+ latitude: number;
16
+ longitude: number;
17
+ hourly: HourlyPoint[];
18
+ stats: {
19
+ temp_min_c: number;
20
+ temp_max_c: number;
21
+ temp_mean_c: number;
22
+ wind_mean_kmh: number;
23
+ solar_mean_wm2: number;
24
+ };
25
+ }
26
+ export declare function getWeatherForecast(params: z.infer<typeof weatherSchema>): Promise<WeatherResult>;
27
+ export {};
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+ import { TtlCache, TTL } from "../lib/cache.js";
3
+ const BASE_URL = "https://api.open-meteo.com/v1/forecast";
4
+ const cache = new TtlCache();
5
+ /** Capital city coordinates for default weather lookups */
6
+ const CAPITAL_COORDS = {
7
+ AT: { lat: 48.21, lon: 16.37, city: "Vienna" },
8
+ BE: { lat: 50.85, lon: 4.35, city: "Brussels" },
9
+ BG: { lat: 42.70, lon: 23.32, city: "Sofia" },
10
+ CH: { lat: 46.95, lon: 7.45, city: "Bern" },
11
+ CZ: { lat: 50.08, lon: 14.42, city: "Prague" },
12
+ DE: { lat: 52.52, lon: 13.41, city: "Berlin" },
13
+ DK: { lat: 55.68, lon: 12.57, city: "Copenhagen" },
14
+ EE: { lat: 59.44, lon: 24.75, city: "Tallinn" },
15
+ ES: { lat: 40.42, lon: -3.70, city: "Madrid" },
16
+ FI: { lat: 60.17, lon: 24.94, city: "Helsinki" },
17
+ FR: { lat: 48.86, lon: 2.35, city: "Paris" },
18
+ GB: { lat: 51.51, lon: -0.13, city: "London" },
19
+ GR: { lat: 37.98, lon: 23.73, city: "Athens" },
20
+ HR: { lat: 45.81, lon: 15.98, city: "Zagreb" },
21
+ HU: { lat: 47.50, lon: 19.04, city: "Budapest" },
22
+ IE: { lat: 53.35, lon: -6.26, city: "Dublin" },
23
+ IT: { lat: 41.90, lon: 12.50, city: "Rome" },
24
+ LT: { lat: 54.69, lon: 25.28, city: "Vilnius" },
25
+ LV: { lat: 56.95, lon: 24.11, city: "Riga" },
26
+ NL: { lat: 52.37, lon: 4.90, city: "Amsterdam" },
27
+ NO: { lat: 59.91, lon: 10.75, city: "Oslo" },
28
+ PL: { lat: 52.23, lon: 21.01, city: "Warsaw" },
29
+ PT: { lat: 38.72, lon: -9.14, city: "Lisbon" },
30
+ RO: { lat: 44.43, lon: 26.10, city: "Bucharest" },
31
+ SE: { lat: 59.33, lon: 18.07, city: "Stockholm" },
32
+ SI: { lat: 46.05, lon: 14.51, city: "Ljubljana" },
33
+ SK: { lat: 48.15, lon: 17.11, city: "Bratislava" },
34
+ };
35
+ export const weatherSchema = z.object({
36
+ country: z
37
+ .string()
38
+ .optional()
39
+ .describe(`Country code for capital city lookup. Available: ${Object.keys(CAPITAL_COORDS).join(", ")}. ` +
40
+ "Ignored if latitude/longitude are provided."),
41
+ latitude: z
42
+ .number()
43
+ .optional()
44
+ .describe("Latitude (-90 to 90). Overrides country-based lookup."),
45
+ longitude: z
46
+ .number()
47
+ .optional()
48
+ .describe("Longitude (-180 to 180). Overrides country-based lookup."),
49
+ });
50
+ function resolveCoords(params) {
51
+ if (params.latitude != null && params.longitude != null) {
52
+ return {
53
+ lat: params.latitude,
54
+ lon: params.longitude,
55
+ location: `${params.latitude},${params.longitude}`,
56
+ };
57
+ }
58
+ if (!params.country) {
59
+ throw new Error("Provide either a country code or latitude/longitude coordinates.");
60
+ }
61
+ const upper = params.country.toUpperCase();
62
+ const coords = CAPITAL_COORDS[upper];
63
+ if (!coords) {
64
+ throw new Error(`Unknown country "${params.country}". Available: ${Object.keys(CAPITAL_COORDS).join(", ")}`);
65
+ }
66
+ return { lat: coords.lat, lon: coords.lon, location: `${coords.city}, ${upper}` };
67
+ }
68
+ export async function getWeatherForecast(params) {
69
+ const { lat, lon, location } = resolveCoords(params);
70
+ const cacheKey = `weather:${lat}:${lon}`;
71
+ const cached = cache.get(cacheKey);
72
+ if (cached)
73
+ return cached;
74
+ const url = new URL(BASE_URL);
75
+ url.searchParams.set("latitude", String(lat));
76
+ url.searchParams.set("longitude", String(lon));
77
+ url.searchParams.set("hourly", "temperature_2m,wind_speed_10m,shortwave_radiation");
78
+ const response = await fetch(url.toString());
79
+ if (!response.ok) {
80
+ const body = await response.text();
81
+ throw new Error(`Open-Meteo API returned ${response.status}: ${body.slice(0, 300)}`);
82
+ }
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ const json = await response.json();
85
+ const h = json.hourly;
86
+ if (!h || !h.time) {
87
+ throw new Error("No hourly forecast data returned.");
88
+ }
89
+ const times = h.time;
90
+ const temps = h.temperature_2m;
91
+ const winds = h.wind_speed_10m;
92
+ const solar = h.shortwave_radiation;
93
+ const hourly = times.map((t, i) => ({
94
+ time: t,
95
+ temperature_c: temps[i] ?? 0,
96
+ wind_speed_kmh: winds[i] ?? 0,
97
+ solar_radiation_wm2: solar[i] ?? 0,
98
+ }));
99
+ const tempValues = temps.filter((v) => v != null);
100
+ const windValues = winds.filter((v) => v != null);
101
+ const solarValues = solar.filter((v) => v != null);
102
+ const mean = (arr) => arr.length > 0
103
+ ? Math.round((arr.reduce((s, v) => s + v, 0) / arr.length) * 100) / 100
104
+ : 0;
105
+ const result = {
106
+ location,
107
+ latitude: lat,
108
+ longitude: lon,
109
+ hourly,
110
+ stats: {
111
+ temp_min_c: tempValues.length > 0 ? Math.min(...tempValues) : 0,
112
+ temp_max_c: tempValues.length > 0 ? Math.max(...tempValues) : 0,
113
+ temp_mean_c: mean(tempValues),
114
+ wind_mean_kmh: mean(windValues),
115
+ solar_mean_wm2: mean(solarValues),
116
+ },
117
+ };
118
+ cache.set(cacheKey, result, TTL.WEATHER);
119
+ return result;
120
+ }