@cliwant/mcp-sam-gov 1.5.0 → 1.7.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 (89) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +248 -231
  3. package/README.ko.md +248 -231
  4. package/README.md +733 -714
  5. package/dist/errors.d.ts +10 -0
  6. package/dist/errors.d.ts.map +1 -1
  7. package/dist/errors.js.map +1 -1
  8. package/dist/feedback.d.ts +64 -0
  9. package/dist/feedback.d.ts.map +1 -0
  10. package/dist/feedback.js +131 -0
  11. package/dist/feedback.js.map +1 -0
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +48 -2
  14. package/dist/server.js.map +1 -1
  15. package/dist/update-check.d.ts +38 -0
  16. package/dist/update-check.d.ts.map +1 -0
  17. package/dist/update-check.js +85 -0
  18. package/dist/update-check.js.map +1 -0
  19. package/package.json +111 -111
  20. package/src/attachments.ts +652 -652
  21. package/src/bea.ts +372 -372
  22. package/src/bls.ts +1943 -1943
  23. package/src/cache.ts +73 -73
  24. package/src/cbp-border.ts +177 -177
  25. package/src/census-economic.ts +431 -431
  26. package/src/census.ts +735 -735
  27. package/src/ckan.ts +495 -495
  28. package/src/clinicaltrials.ts +923 -923
  29. package/src/cms-facility.ts +379 -379
  30. package/src/cms-hospital.ts +344 -344
  31. package/src/cms-supplier.ts +527 -527
  32. package/src/cms-utilization.ts +389 -389
  33. package/src/cms.ts +634 -634
  34. package/src/coerce.ts +47 -47
  35. package/src/courtlistener.ts +465 -465
  36. package/src/cpsc.ts +333 -333
  37. package/src/datagov-catalog.ts +312 -312
  38. package/src/datagov.ts +907 -907
  39. package/src/datagovKey.ts +68 -68
  40. package/src/datasource.ts +721 -721
  41. package/src/disclosure.ts +61 -61
  42. package/src/dol.ts +515 -515
  43. package/src/ecfr.ts +248 -248
  44. package/src/echo.ts +496 -496
  45. package/src/edgar.ts +3046 -3046
  46. package/src/epa-envirofacts.ts +358 -358
  47. package/src/errors.ts +324 -314
  48. package/src/fac.ts +529 -529
  49. package/src/far.ts +1009 -1009
  50. package/src/fdic.ts +2052 -2052
  51. package/src/federal-register.ts +725 -725
  52. package/src/feedback.ts +160 -0
  53. package/src/fema.ts +680 -680
  54. package/src/fpds.ts +620 -620
  55. package/src/fred.ts +464 -464
  56. package/src/gao.ts +744 -744
  57. package/src/gov-domains.ts +237 -237
  58. package/src/govinfo.ts +497 -497
  59. package/src/grants.ts +290 -290
  60. package/src/gsa-csv.ts +992 -992
  61. package/src/gsa-perdiem.ts +361 -361
  62. package/src/integrity.ts +928 -928
  63. package/src/keys.ts +268 -268
  64. package/src/lda.ts +385 -385
  65. package/src/meta.ts +292 -292
  66. package/src/nhtsa.ts +352 -352
  67. package/src/nih.ts +375 -375
  68. package/src/nist-controls.ts +219 -219
  69. package/src/nonprofit.ts +460 -460
  70. package/src/nppes.ts +834 -834
  71. package/src/nsf.ts +706 -706
  72. package/src/nvd.ts +1124 -1124
  73. package/src/nws-weather.ts +167 -167
  74. package/src/ofac.ts +1166 -1166
  75. package/src/openfda-device.ts +356 -356
  76. package/src/openfda-drugsfda.ts +313 -313
  77. package/src/openfda.ts +518 -518
  78. package/src/pricing.ts +1075 -1075
  79. package/src/sam-gov/client.ts +774 -774
  80. package/src/sam-gov/index.ts +32 -32
  81. package/src/sam-gov/types.ts +152 -152
  82. package/src/sba.ts +357 -357
  83. package/src/server.ts +6692 -6639
  84. package/src/snapshot.ts +223 -223
  85. package/src/socrata.ts +532 -532
  86. package/src/treasury.ts +582 -582
  87. package/src/update-check.ts +88 -0
  88. package/src/usaspending.ts +2852 -2852
  89. package/src/usitc.ts +420 -420
package/src/keys.ts CHANGED
@@ -1,268 +1,268 @@
1
- /**
2
- * @cliwant/mcp-sam-gov/keys — API-key discovery + `.env` auto-loading.
3
- *
4
- * Why this exists
5
- * ----------------
6
- * The server rides dozens of federal data sources. MOST are fully keyless. But the set of
7
- * *optional* keys (raise a rate limit, unlock one filter) plus the four *required*
8
- * keys (Census business-patterns, FRED, BEA Regional, DOL data) has grown to the point where a user — or
9
- * the AI driving the server — cannot tell, without reading source code:
10
- * - which env var each source reads,
11
- * - whether a key is REQUIRED or merely OPTIONAL,
12
- * - where to get one (free), and
13
- * - whether it is currently configured.
14
- *
15
- * `apiKeyStatus()` answers all four, truthfully, WITHOUT ever revealing a key's
16
- * value (only a `currentlySet` boolean). `loadDotEnv()` lets a user configure
17
- * keys ONCE in a `.env` file instead of the host's env block.
18
- *
19
- * Grounding: every `envVar` below is the exact string the code reads via
20
- * `process.env.<NAME>` — DATA_GOV_API_KEY (datagovKey.ts), SAM_GOV_API_KEY
21
- * (server.ts), BLS_API_KEY (bls.ts), NVD_API_KEY (nvd.ts), SOCRATA_APP_TOKEN
22
- * (socrata.ts), CENSUS_API_KEY (census-economic.ts), FRED_API_KEY (fred.ts),
23
- * BEA_API_KEY (bea.ts), DOL_API_KEY (dol.ts), LDA_API_KEY (lda.ts),
24
- * OPENFDA_API_KEY (openfda.ts), COURTLISTENER_API_TOKEN (courtlistener.ts).
25
- * No invented keys, sources, or signup URLs.
26
- */
27
-
28
- import { readFileSync } from "node:fs";
29
- import { join } from "node:path";
30
-
31
- /** One registry entry describing a single API key the server can use. */
32
- export type KeyRegistryEntry = {
33
- /** The exact `process.env.<NAME>` the code reads. */
34
- envVar: string;
35
- /** Human-readable source(s) this key affects. */
36
- sources: string[];
37
- /** true ⇒ the source has NO keyless tier (the tool throws without it). */
38
- required: boolean;
39
- /** Free signup URL (the user creates the account — this is their step). */
40
- signupUrl: string;
41
- /** What setting the key unlocks (higher limit / a filter / a whole tool). */
42
- unlocks: string;
43
- /** Extra honesty note (keyless fallback, precedence, scope). */
44
- note: string;
45
- };
46
-
47
- /**
48
- * The 12 keys the server reads — code-grounded, no inventions.
49
- *
50
- * REQUIRED (4): CENSUS_API_KEY, FRED_API_KEY, BEA_API_KEY, DOL_API_KEY — those sources
51
- * have no keyless tier, so the tool throws without them (DOL_API_KEY gates ONLY
52
- * dol_get_dataset; the DOL catalog, dol_list_datasets, is keyless). OPTIONAL (8):
53
- * everything else works keyless; a key only raises a rate limit or unlocks a single filter.
54
- */
55
- export const KEY_REGISTRY: readonly KeyRegistryEntry[] = [
56
- {
57
- envVar: "DATA_GOV_API_KEY",
58
- sources: [
59
- "api.data.gov keyed sources: Regulations.gov, Congress.gov, GovInfo, Federal Audit Clearinghouse (FAC), data.gov catalog, GSA per-diem",
60
- ],
61
- required: false,
62
- signupUrl: "https://api.data.gov/signup/",
63
- unlocks:
64
- "higher rate limits on the api.data.gov keyed sources (lifts the shared DEMO_KEY ~30/hr cap to ~1,000/hr)",
65
- note: "Keyless by default via the public DEMO_KEY; a key only raises the shared hourly quota. (NPPES, CMS, and Federal Register are keyless on their own hosts and do NOT use this key.)",
66
- },
67
- {
68
- envVar: "SAM_GOV_API_KEY",
69
- sources: ["SAM.gov opportunities"],
70
- required: false,
71
- signupUrl:
72
- "https://open.gsa.gov/api/get-opportunities-public-api/",
73
- unlocks:
74
- "the authenticated v2 opportunity search + the organization-name filter",
75
- note: "Keyless HAL endpoint works without it; a key enables the keyed v2 path and org-name filtering. Register at sam.gov / api.sam.gov.",
76
- },
77
- {
78
- envVar: "BLS_API_KEY",
79
- sources: ["Bureau of Labor Statistics (BLS)"],
80
- required: false,
81
- signupUrl: "https://data.bls.gov/registrationEngine/",
82
- unlocks:
83
- "the BLS v2 tier (~500 queries/day, 50 series/query, ~20-year span) vs keyless v1 (~25 queries/day)",
84
- note: "Keyless v1 works out of the box; a key upgrades to the higher v2 limits.",
85
- },
86
- {
87
- envVar: "NVD_API_KEY",
88
- sources: ["NIST NVD (cve_lookup)"],
89
- required: false,
90
- signupUrl: "https://nvd.nist.gov/developers/request-an-api-key",
91
- unlocks: "a higher NVD rate limit",
92
- note: "Keyless by default; a key lifts the request rate limit.",
93
- },
94
- {
95
- envVar: "SOCRATA_APP_TOKEN",
96
- sources: ["Socrata (state/city open-data portals)"],
97
- required: false,
98
- signupUrl: "https://evergreen.data.socrata.com/signup",
99
- unlocks: "higher Socrata throttling limits",
100
- note: "Keyless by default; a token raises the per-host throttle. Any Socrata portal's developer settings issues one.",
101
- },
102
- {
103
- envVar: "CENSUS_API_KEY",
104
- sources: ["US Census (census_business_patterns)"],
105
- required: true,
106
- signupUrl: "https://api.census.gov/data/key_signup.html",
107
- unlocks:
108
- "the census_business_patterns tool (there is no keyless tier — it throws without a key)",
109
- note: "REQUIRED: the Census economic API has no keyless access.",
110
- },
111
- {
112
- envVar: "FRED_API_KEY",
113
- sources: ["FRED (fred_search_series, fred_series_observations)"],
114
- required: true,
115
- signupUrl: "https://fred.stlouisfed.org/docs/api/api_key.html",
116
- unlocks:
117
- "the 2 FRED tools (there is no keyless tier — they throw without a key)",
118
- note: "REQUIRED: the FRED API has no keyless access.",
119
- },
120
- {
121
- envVar: "BEA_API_KEY",
122
- sources: ["BEA Regional Economic Accounts (bea_regional_data)"],
123
- required: true,
124
- signupUrl: "https://apps.bea.gov/API/signup/",
125
- unlocks:
126
- "the bea_regional_data tool (there is no keyless tier — it throws without a key)",
127
- note: "REQUIRED: the BEA Data API has no keyless access.",
128
- },
129
- {
130
- envVar: "DOL_API_KEY",
131
- sources: [
132
- "US DOL enforcement data (dol_get_dataset; dol_list_datasets is keyless)",
133
- ],
134
- required: true,
135
- signupUrl: "https://dataportal.dol.gov/registration",
136
- unlocks:
137
- "the dol_get_dataset tool (dataset records need a free key; dol_list_datasets works keyless)",
138
- note: "The DOL data endpoint needs a free key; the dataset CATALOG (dol_list_datasets) and agency list are keyless.",
139
- },
140
- {
141
- envVar: "LDA_API_KEY",
142
- sources: ["US Senate LDA lobbying (lda_search_filings)"],
143
- required: false,
144
- signupUrl: "https://lda.senate.gov/api/register/",
145
- unlocks:
146
- "higher LDA API rate limits (anonymous access already works without it)",
147
- note: "Keyless by default (anonymous 200); a free token only raises the rate limit.",
148
- },
149
- {
150
- envVar: "OPENFDA_API_KEY",
151
- sources: ["openFDA enforcement (openfda_enforcement)"],
152
- required: false,
153
- signupUrl: "https://open.fda.gov/apis/authentication/",
154
- unlocks:
155
- "higher openFDA rate limits (keyless works without it — ~1000/day)",
156
- note: "Keyless by default; a free key raises the rate limit.",
157
- },
158
- {
159
- envVar: "COURTLISTENER_API_TOKEN",
160
- sources: [
161
- "CourtListener federal court opinions (courtlistener_search_opinions)",
162
- ],
163
- required: false,
164
- signupUrl: "https://www.courtlistener.com/help/api/rest/",
165
- unlocks:
166
- "higher CourtListener rate limits (anonymous search works without it)",
167
- note: "Keyless by default (anonymous 200); a free token only raises the rate limit. Data = US federal court public records via CourtListener/Free Law Project.",
168
- },
169
- ] as const;
170
-
171
- /** true iff the env var is set to a non-empty (after-trim) string. */
172
- function isSet(envVar: string): boolean {
173
- const v = process.env[envVar];
174
- return typeof v === "string" && v.trim().length > 0;
175
- }
176
-
177
- /** Per-key status: the registry entry + a `currentlySet` boolean. NEVER the value. */
178
- export type KeyStatus = KeyRegistryEntry & { currentlySet: boolean };
179
-
180
- /** The `apiKeyStatus()` result shape. */
181
- export type ApiKeyStatusResult = {
182
- keys: KeyStatus[];
183
- /** envVars of REQUIRED keys not currently set (empty ⇒ all required keys present). */
184
- requiredMissing: string[];
185
- /** envVars of OPTIONAL keys not currently set. */
186
- optionalMissing: string[];
187
- /** Every key here is free to obtain. */
188
- allKeysFree: boolean;
189
- };
190
-
191
- /**
192
- * Report which API keys the server can use and whether each is configured.
193
- *
194
- * SECURITY: the returned object carries ONLY a `currentlySet` boolean per key —
195
- * the key's VALUE is NEVER read into the output. (`isSet` inspects the value to
196
- * compute the boolean, but the value itself never leaves this function.)
197
- */
198
- export function apiKeyStatus(): ApiKeyStatusResult {
199
- const keys: KeyStatus[] = KEY_REGISTRY.map((k) => ({
200
- ...k,
201
- currentlySet: isSet(k.envVar),
202
- }));
203
- const requiredMissing = keys
204
- .filter((k) => k.required && !k.currentlySet)
205
- .map((k) => k.envVar);
206
- const optionalMissing = keys
207
- .filter((k) => !k.required && !k.currentlySet)
208
- .map((k) => k.envVar);
209
- return { keys, requiredMissing, optionalMissing, allKeysFree: true };
210
- }
211
-
212
- /**
213
- * MINIMAL, dependency-free `.env` loader.
214
- *
215
- * Reads `${cwd||process.cwd()}/.env` if present and sets `process.env[KEY]` for
216
- * each `KEY=VALUE` line — but ONLY if that key is not already set, so a real
217
- * environment variable always wins over `.env` (standard precedence). Supports
218
- * `export KEY=VALUE`, `#` comments, blank lines, and surrounding single/double
219
- * quotes on the value. NEVER throws: a missing file returns 0 (⇒ byte-identical
220
- * startup), and a malformed line is skipped rather than fatal.
221
- *
222
- * @returns the number of vars newly set into process.env.
223
- */
224
- export function loadDotEnv(cwd?: string): number {
225
- const path = join(cwd ?? process.cwd(), ".env");
226
- let text: string;
227
- try {
228
- text = readFileSync(path, "utf8");
229
- } catch {
230
- // Missing / unreadable .env ⇒ zero change. This is the common case and
231
- // MUST be a no-op so startup is byte-identical when no .env exists.
232
- return 0;
233
- }
234
-
235
- let loaded = 0;
236
- for (const rawLine of text.split(/\r?\n/)) {
237
- const line = rawLine.trim();
238
- if (line.length === 0 || line.startsWith("#")) continue;
239
-
240
- // Optional `export ` prefix.
241
- const body = line.startsWith("export ")
242
- ? line.slice("export ".length).trim()
243
- : line;
244
-
245
- const eq = body.indexOf("=");
246
- if (eq <= 0) continue; // no `=`, or empty key ⇒ skip (malformed).
247
-
248
- const key = body.slice(0, eq).trim();
249
- if (!key) continue;
250
-
251
- let value = body.slice(eq + 1).trim();
252
- // Strip a single matching pair of surrounding quotes.
253
- if (
254
- value.length >= 2 &&
255
- ((value.startsWith('"') && value.endsWith('"')) ||
256
- (value.startsWith("'") && value.endsWith("'")))
257
- ) {
258
- value = value.slice(1, -1);
259
- }
260
-
261
- // Precedence: if the key is already set in the real environment, it wins —
262
- // we set `process.env[key]` ONLY when it is not already present.
263
- if (process.env[key] !== undefined) continue;
264
- process.env[key] = value;
265
- loaded++;
266
- }
267
- return loaded;
268
- }
1
+ /**
2
+ * @cliwant/mcp-sam-gov/keys — API-key discovery + `.env` auto-loading.
3
+ *
4
+ * Why this exists
5
+ * ----------------
6
+ * The server rides dozens of federal data sources. MOST are fully keyless. But the set of
7
+ * *optional* keys (raise a rate limit, unlock one filter) plus the four *required*
8
+ * keys (Census business-patterns, FRED, BEA Regional, DOL data) has grown to the point where a user — or
9
+ * the AI driving the server — cannot tell, without reading source code:
10
+ * - which env var each source reads,
11
+ * - whether a key is REQUIRED or merely OPTIONAL,
12
+ * - where to get one (free), and
13
+ * - whether it is currently configured.
14
+ *
15
+ * `apiKeyStatus()` answers all four, truthfully, WITHOUT ever revealing a key's
16
+ * value (only a `currentlySet` boolean). `loadDotEnv()` lets a user configure
17
+ * keys ONCE in a `.env` file instead of the host's env block.
18
+ *
19
+ * Grounding: every `envVar` below is the exact string the code reads via
20
+ * `process.env.<NAME>` — DATA_GOV_API_KEY (datagovKey.ts), SAM_GOV_API_KEY
21
+ * (server.ts), BLS_API_KEY (bls.ts), NVD_API_KEY (nvd.ts), SOCRATA_APP_TOKEN
22
+ * (socrata.ts), CENSUS_API_KEY (census-economic.ts), FRED_API_KEY (fred.ts),
23
+ * BEA_API_KEY (bea.ts), DOL_API_KEY (dol.ts), LDA_API_KEY (lda.ts),
24
+ * OPENFDA_API_KEY (openfda.ts), COURTLISTENER_API_TOKEN (courtlistener.ts).
25
+ * No invented keys, sources, or signup URLs.
26
+ */
27
+
28
+ import { readFileSync } from "node:fs";
29
+ import { join } from "node:path";
30
+
31
+ /** One registry entry describing a single API key the server can use. */
32
+ export type KeyRegistryEntry = {
33
+ /** The exact `process.env.<NAME>` the code reads. */
34
+ envVar: string;
35
+ /** Human-readable source(s) this key affects. */
36
+ sources: string[];
37
+ /** true ⇒ the source has NO keyless tier (the tool throws without it). */
38
+ required: boolean;
39
+ /** Free signup URL (the user creates the account — this is their step). */
40
+ signupUrl: string;
41
+ /** What setting the key unlocks (higher limit / a filter / a whole tool). */
42
+ unlocks: string;
43
+ /** Extra honesty note (keyless fallback, precedence, scope). */
44
+ note: string;
45
+ };
46
+
47
+ /**
48
+ * The 12 keys the server reads — code-grounded, no inventions.
49
+ *
50
+ * REQUIRED (4): CENSUS_API_KEY, FRED_API_KEY, BEA_API_KEY, DOL_API_KEY — those sources
51
+ * have no keyless tier, so the tool throws without them (DOL_API_KEY gates ONLY
52
+ * dol_get_dataset; the DOL catalog, dol_list_datasets, is keyless). OPTIONAL (8):
53
+ * everything else works keyless; a key only raises a rate limit or unlocks a single filter.
54
+ */
55
+ export const KEY_REGISTRY: readonly KeyRegistryEntry[] = [
56
+ {
57
+ envVar: "DATA_GOV_API_KEY",
58
+ sources: [
59
+ "api.data.gov keyed sources: Regulations.gov, Congress.gov, GovInfo, Federal Audit Clearinghouse (FAC), data.gov catalog, GSA per-diem",
60
+ ],
61
+ required: false,
62
+ signupUrl: "https://api.data.gov/signup/",
63
+ unlocks:
64
+ "higher rate limits on the api.data.gov keyed sources (lifts the shared DEMO_KEY ~30/hr cap to ~1,000/hr)",
65
+ note: "Keyless by default via the public DEMO_KEY; a key only raises the shared hourly quota. (NPPES, CMS, and Federal Register are keyless on their own hosts and do NOT use this key.)",
66
+ },
67
+ {
68
+ envVar: "SAM_GOV_API_KEY",
69
+ sources: ["SAM.gov opportunities"],
70
+ required: false,
71
+ signupUrl:
72
+ "https://open.gsa.gov/api/get-opportunities-public-api/",
73
+ unlocks:
74
+ "the authenticated v2 opportunity search + the organization-name filter",
75
+ note: "Keyless HAL endpoint works without it; a key enables the keyed v2 path and org-name filtering. Register at sam.gov / api.sam.gov.",
76
+ },
77
+ {
78
+ envVar: "BLS_API_KEY",
79
+ sources: ["Bureau of Labor Statistics (BLS)"],
80
+ required: false,
81
+ signupUrl: "https://data.bls.gov/registrationEngine/",
82
+ unlocks:
83
+ "the BLS v2 tier (~500 queries/day, 50 series/query, ~20-year span) vs keyless v1 (~25 queries/day)",
84
+ note: "Keyless v1 works out of the box; a key upgrades to the higher v2 limits.",
85
+ },
86
+ {
87
+ envVar: "NVD_API_KEY",
88
+ sources: ["NIST NVD (cve_lookup)"],
89
+ required: false,
90
+ signupUrl: "https://nvd.nist.gov/developers/request-an-api-key",
91
+ unlocks: "a higher NVD rate limit",
92
+ note: "Keyless by default; a key lifts the request rate limit.",
93
+ },
94
+ {
95
+ envVar: "SOCRATA_APP_TOKEN",
96
+ sources: ["Socrata (state/city open-data portals)"],
97
+ required: false,
98
+ signupUrl: "https://evergreen.data.socrata.com/signup",
99
+ unlocks: "higher Socrata throttling limits",
100
+ note: "Keyless by default; a token raises the per-host throttle. Any Socrata portal's developer settings issues one.",
101
+ },
102
+ {
103
+ envVar: "CENSUS_API_KEY",
104
+ sources: ["US Census (census_business_patterns)"],
105
+ required: true,
106
+ signupUrl: "https://api.census.gov/data/key_signup.html",
107
+ unlocks:
108
+ "the census_business_patterns tool (there is no keyless tier — it throws without a key)",
109
+ note: "REQUIRED: the Census economic API has no keyless access.",
110
+ },
111
+ {
112
+ envVar: "FRED_API_KEY",
113
+ sources: ["FRED (fred_search_series, fred_series_observations)"],
114
+ required: true,
115
+ signupUrl: "https://fred.stlouisfed.org/docs/api/api_key.html",
116
+ unlocks:
117
+ "the 2 FRED tools (there is no keyless tier — they throw without a key)",
118
+ note: "REQUIRED: the FRED API has no keyless access.",
119
+ },
120
+ {
121
+ envVar: "BEA_API_KEY",
122
+ sources: ["BEA Regional Economic Accounts (bea_regional_data)"],
123
+ required: true,
124
+ signupUrl: "https://apps.bea.gov/API/signup/",
125
+ unlocks:
126
+ "the bea_regional_data tool (there is no keyless tier — it throws without a key)",
127
+ note: "REQUIRED: the BEA Data API has no keyless access.",
128
+ },
129
+ {
130
+ envVar: "DOL_API_KEY",
131
+ sources: [
132
+ "US DOL enforcement data (dol_get_dataset; dol_list_datasets is keyless)",
133
+ ],
134
+ required: true,
135
+ signupUrl: "https://dataportal.dol.gov/registration",
136
+ unlocks:
137
+ "the dol_get_dataset tool (dataset records need a free key; dol_list_datasets works keyless)",
138
+ note: "The DOL data endpoint needs a free key; the dataset CATALOG (dol_list_datasets) and agency list are keyless.",
139
+ },
140
+ {
141
+ envVar: "LDA_API_KEY",
142
+ sources: ["US Senate LDA lobbying (lda_search_filings)"],
143
+ required: false,
144
+ signupUrl: "https://lda.senate.gov/api/register/",
145
+ unlocks:
146
+ "higher LDA API rate limits (anonymous access already works without it)",
147
+ note: "Keyless by default (anonymous 200); a free token only raises the rate limit.",
148
+ },
149
+ {
150
+ envVar: "OPENFDA_API_KEY",
151
+ sources: ["openFDA enforcement (openfda_enforcement)"],
152
+ required: false,
153
+ signupUrl: "https://open.fda.gov/apis/authentication/",
154
+ unlocks:
155
+ "higher openFDA rate limits (keyless works without it — ~1000/day)",
156
+ note: "Keyless by default; a free key raises the rate limit.",
157
+ },
158
+ {
159
+ envVar: "COURTLISTENER_API_TOKEN",
160
+ sources: [
161
+ "CourtListener federal court opinions (courtlistener_search_opinions)",
162
+ ],
163
+ required: false,
164
+ signupUrl: "https://www.courtlistener.com/help/api/rest/",
165
+ unlocks:
166
+ "higher CourtListener rate limits (anonymous search works without it)",
167
+ note: "Keyless by default (anonymous 200); a free token only raises the rate limit. Data = US federal court public records via CourtListener/Free Law Project.",
168
+ },
169
+ ] as const;
170
+
171
+ /** true iff the env var is set to a non-empty (after-trim) string. */
172
+ function isSet(envVar: string): boolean {
173
+ const v = process.env[envVar];
174
+ return typeof v === "string" && v.trim().length > 0;
175
+ }
176
+
177
+ /** Per-key status: the registry entry + a `currentlySet` boolean. NEVER the value. */
178
+ export type KeyStatus = KeyRegistryEntry & { currentlySet: boolean };
179
+
180
+ /** The `apiKeyStatus()` result shape. */
181
+ export type ApiKeyStatusResult = {
182
+ keys: KeyStatus[];
183
+ /** envVars of REQUIRED keys not currently set (empty ⇒ all required keys present). */
184
+ requiredMissing: string[];
185
+ /** envVars of OPTIONAL keys not currently set. */
186
+ optionalMissing: string[];
187
+ /** Every key here is free to obtain. */
188
+ allKeysFree: boolean;
189
+ };
190
+
191
+ /**
192
+ * Report which API keys the server can use and whether each is configured.
193
+ *
194
+ * SECURITY: the returned object carries ONLY a `currentlySet` boolean per key —
195
+ * the key's VALUE is NEVER read into the output. (`isSet` inspects the value to
196
+ * compute the boolean, but the value itself never leaves this function.)
197
+ */
198
+ export function apiKeyStatus(): ApiKeyStatusResult {
199
+ const keys: KeyStatus[] = KEY_REGISTRY.map((k) => ({
200
+ ...k,
201
+ currentlySet: isSet(k.envVar),
202
+ }));
203
+ const requiredMissing = keys
204
+ .filter((k) => k.required && !k.currentlySet)
205
+ .map((k) => k.envVar);
206
+ const optionalMissing = keys
207
+ .filter((k) => !k.required && !k.currentlySet)
208
+ .map((k) => k.envVar);
209
+ return { keys, requiredMissing, optionalMissing, allKeysFree: true };
210
+ }
211
+
212
+ /**
213
+ * MINIMAL, dependency-free `.env` loader.
214
+ *
215
+ * Reads `${cwd||process.cwd()}/.env` if present and sets `process.env[KEY]` for
216
+ * each `KEY=VALUE` line — but ONLY if that key is not already set, so a real
217
+ * environment variable always wins over `.env` (standard precedence). Supports
218
+ * `export KEY=VALUE`, `#` comments, blank lines, and surrounding single/double
219
+ * quotes on the value. NEVER throws: a missing file returns 0 (⇒ byte-identical
220
+ * startup), and a malformed line is skipped rather than fatal.
221
+ *
222
+ * @returns the number of vars newly set into process.env.
223
+ */
224
+ export function loadDotEnv(cwd?: string): number {
225
+ const path = join(cwd ?? process.cwd(), ".env");
226
+ let text: string;
227
+ try {
228
+ text = readFileSync(path, "utf8");
229
+ } catch {
230
+ // Missing / unreadable .env ⇒ zero change. This is the common case and
231
+ // MUST be a no-op so startup is byte-identical when no .env exists.
232
+ return 0;
233
+ }
234
+
235
+ let loaded = 0;
236
+ for (const rawLine of text.split(/\r?\n/)) {
237
+ const line = rawLine.trim();
238
+ if (line.length === 0 || line.startsWith("#")) continue;
239
+
240
+ // Optional `export ` prefix.
241
+ const body = line.startsWith("export ")
242
+ ? line.slice("export ".length).trim()
243
+ : line;
244
+
245
+ const eq = body.indexOf("=");
246
+ if (eq <= 0) continue; // no `=`, or empty key ⇒ skip (malformed).
247
+
248
+ const key = body.slice(0, eq).trim();
249
+ if (!key) continue;
250
+
251
+ let value = body.slice(eq + 1).trim();
252
+ // Strip a single matching pair of surrounding quotes.
253
+ if (
254
+ value.length >= 2 &&
255
+ ((value.startsWith('"') && value.endsWith('"')) ||
256
+ (value.startsWith("'") && value.endsWith("'")))
257
+ ) {
258
+ value = value.slice(1, -1);
259
+ }
260
+
261
+ // Precedence: if the key is already set in the real environment, it wins —
262
+ // we set `process.env[key]` ONLY when it is not already present.
263
+ if (process.env[key] !== undefined) continue;
264
+ process.env[key] = value;
265
+ loaded++;
266
+ }
267
+ return loaded;
268
+ }