tn-elections-mcp 0.5.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 (77) hide show
  1. package/README.md +87 -0
  2. package/dist/api/easyvote-client.d.ts +51 -0
  3. package/dist/api/easyvote-client.js +254 -0
  4. package/dist/api/easyvote-client.js.map +1 -0
  5. package/dist/api/easyvote-client.test.d.ts +1 -0
  6. package/dist/api/easyvote-client.test.js +47 -0
  7. package/dist/api/easyvote-client.test.js.map +1 -0
  8. package/dist/api/gemini-pdf.d.ts +23 -0
  9. package/dist/api/gemini-pdf.js +261 -0
  10. package/dist/api/gemini-pdf.js.map +1 -0
  11. package/dist/api/sos-client.d.ts +15 -0
  12. package/dist/api/sos-client.js +74 -0
  13. package/dist/api/sos-client.js.map +1 -0
  14. package/dist/api/sumner-client.d.ts +12 -0
  15. package/dist/api/sumner-client.js +57 -0
  16. package/dist/api/sumner-client.js.map +1 -0
  17. package/dist/api/tncamp-client.d.ts +61 -0
  18. package/dist/api/tncamp-client.js +310 -0
  19. package/dist/api/tncamp-client.js.map +1 -0
  20. package/dist/data/sumner-cycles.d.ts +3 -0
  21. package/dist/data/sumner-cycles.js +19 -0
  22. package/dist/data/sumner-cycles.js.map +1 -0
  23. package/dist/data/sumner-cycles.test.d.ts +1 -0
  24. package/dist/data/sumner-cycles.test.js +29 -0
  25. package/dist/data/sumner-cycles.test.js.map +1 -0
  26. package/dist/data/tn-counties.d.ts +23 -0
  27. package/dist/data/tn-counties.js +162 -0
  28. package/dist/data/tn-counties.js.map +1 -0
  29. package/dist/data/tn-counties.test.d.ts +1 -0
  30. package/dist/data/tn-counties.test.js +71 -0
  31. package/dist/data/tn-counties.test.js.map +1 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +31 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/parsers/sos-xlsx.d.ts +24 -0
  36. package/dist/parsers/sos-xlsx.js +149 -0
  37. package/dist/parsers/sos-xlsx.js.map +1 -0
  38. package/dist/parsers/sos-xlsx.test.d.ts +1 -0
  39. package/dist/parsers/sos-xlsx.test.js +111 -0
  40. package/dist/parsers/sos-xlsx.test.js.map +1 -0
  41. package/dist/parsers/sumner-html.d.ts +8 -0
  42. package/dist/parsers/sumner-html.js +200 -0
  43. package/dist/parsers/sumner-html.js.map +1 -0
  44. package/dist/parsers/sumner-html.test.d.ts +1 -0
  45. package/dist/parsers/sumner-html.test.js +79 -0
  46. package/dist/parsers/sumner-html.test.js.map +1 -0
  47. package/dist/parsers/tncamp-form.d.ts +15 -0
  48. package/dist/parsers/tncamp-form.js +54 -0
  49. package/dist/parsers/tncamp-form.js.map +1 -0
  50. package/dist/parsers/tncamp-form.test.d.ts +1 -0
  51. package/dist/parsers/tncamp-form.test.js +114 -0
  52. package/dist/parsers/tncamp-form.test.js.map +1 -0
  53. package/dist/parsers/tncamp-report.d.ts +6 -0
  54. package/dist/parsers/tncamp-report.js +189 -0
  55. package/dist/parsers/tncamp-report.js.map +1 -0
  56. package/dist/parsers/tncamp-results.d.ts +10 -0
  57. package/dist/parsers/tncamp-results.js +113 -0
  58. package/dist/parsers/tncamp-results.js.map +1 -0
  59. package/dist/parsers/tncamp-results.test.d.ts +1 -0
  60. package/dist/parsers/tncamp-results.test.js +86 -0
  61. package/dist/parsers/tncamp-results.test.js.map +1 -0
  62. package/dist/tools/easyvote.d.ts +2 -0
  63. package/dist/tools/easyvote.js +382 -0
  64. package/dist/tools/easyvote.js.map +1 -0
  65. package/dist/tools/sos.d.ts +2 -0
  66. package/dist/tools/sos.js +130 -0
  67. package/dist/tools/sos.js.map +1 -0
  68. package/dist/tools/sumner.d.ts +2 -0
  69. package/dist/tools/sumner.js +134 -0
  70. package/dist/tools/sumner.js.map +1 -0
  71. package/dist/tools/tncamp.d.ts +2 -0
  72. package/dist/tools/tncamp.js +401 -0
  73. package/dist/tools/tncamp.js.map +1 -0
  74. package/dist/types.d.ts +270 -0
  75. package/dist/types.js +5 -0
  76. package/dist/types.js.map +1 -0
  77. package/package.json +47 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * PDF financial data extraction.
3
+ *
4
+ * Primary: Gemini Flash (cheap, fast, handles PDFs natively)
5
+ * Fallback: Download PDF → pdftoppm → JPEG images → return as base64
6
+ * for Claude to read directly via MCP image content.
7
+ *
8
+ * The fallback works because these are scanned PDFs (pdftotext returns empty).
9
+ */
10
+ import { execFile } from 'child_process';
11
+ import { promisify } from 'util';
12
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from 'fs';
13
+ import { join } from 'path';
14
+ import { tmpdir } from 'os';
15
+ const GEMINI_MODEL = 'gemini-2.5-flash';
16
+ const MAX_PDF_SIZE = 10 * 1024 * 1024;
17
+ const reportCache = new Map();
18
+ const CACHE_TTL_MS = 30 * 60 * 1000;
19
+ function getApiKey() {
20
+ return process.env.GEMINI_API_KEY || null;
21
+ }
22
+ const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
23
+ async function fetchPdf(url) {
24
+ const response = await fetch(url);
25
+ if (!response.ok) {
26
+ throw new Error(`Failed to download PDF: ${response.status} ${response.statusText}`);
27
+ }
28
+ const buffer = Buffer.from(await response.arrayBuffer());
29
+ if (buffer.length > MAX_PDF_SIZE) {
30
+ throw new Error(`PDF too large: ${(buffer.length / 1024 / 1024).toFixed(1)}MB (max ${MAX_PDF_SIZE / 1024 / 1024}MB)`);
31
+ }
32
+ return buffer;
33
+ }
34
+ const EXTRACTION_PROMPT = `Extract ALL data from this Tennessee campaign finance disclosure PDF into valid JSON.
35
+
36
+ {
37
+ "candidateName": "string — full name as printed",
38
+ "officeSought": "string",
39
+ "reportPeriod": "string — e.g. 'Mid-Year 2023', 'Year-End 2025'",
40
+ "beginningBalance": number,
41
+ "totalContributions": number,
42
+ "totalExpenditures": number,
43
+ "endingBalance": number,
44
+ "contributions": [
45
+ {"date": "MM/DD/YYYY", "name": "contributor name", "address": "full address", "amount": number, "type": "Monetary", "employer": "string or null", "occupation": "string or null"}
46
+ ],
47
+ "expenditures": [
48
+ {"date": "MM/DD/YYYY", "payee": "payee name", "address": "full address", "amount": number, "purpose": "string or null"}
49
+ ],
50
+ "warnings": ["array of any extraction issues or unclear values"]
51
+ }
52
+
53
+ Rules:
54
+ - Return ONLY the JSON object. No markdown fencing, no explanation.
55
+ - Amounts must be numbers. $1,500.00 becomes 1500.00.
56
+ - Empty sections get empty arrays.
57
+ - If not a campaign finance report, return {"error": "Not a campaign finance report"}.
58
+ - Include ALL itemized entries. Do not truncate.
59
+ - Default contribution type to "Monetary" unless clearly In-Kind or Loan.`;
60
+ function parseGeminiResponse(response) {
61
+ if (response.error) {
62
+ throw new Error(`Gemini API error: ${response.error.message}`);
63
+ }
64
+ const candidate = (response.candidates || [])[0];
65
+ if (!candidate)
66
+ throw new Error('Gemini returned no candidates');
67
+ const finishReason = candidate.finishReason || 'UNKNOWN';
68
+ const parts = candidate.content?.parts || [];
69
+ const texts = parts.filter(p => p.text).map(p => p.text);
70
+ if (texts.length === 0) {
71
+ throw new Error(`Gemini returned no text (finishReason: ${finishReason})`);
72
+ }
73
+ return { text: texts[texts.length - 1].trim(), finishReason };
74
+ }
75
+ function cleanJsonResponse(raw) {
76
+ let cleaned = raw;
77
+ if (cleaned.startsWith('```json'))
78
+ cleaned = cleaned.slice(7);
79
+ else if (cleaned.startsWith('```'))
80
+ cleaned = cleaned.slice(3);
81
+ if (cleaned.endsWith('```'))
82
+ cleaned = cleaned.slice(0, -3);
83
+ cleaned = cleaned.trim();
84
+ cleaned = cleaned.replace(/,\s*}/g, '}').replace(/,\s*]/g, ']');
85
+ return cleaned;
86
+ }
87
+ function determineConfidence(parsed, warnings) {
88
+ if (warnings.length === 0 && parsed.candidateName && parsed.endingBalance !== undefined)
89
+ return 'high';
90
+ if (warnings.length <= 2)
91
+ return 'medium';
92
+ return 'low';
93
+ }
94
+ function buildReport(parsed, reportName, model, extraWarnings) {
95
+ const geminiWarnings = Array.isArray(parsed.warnings) ? parsed.warnings.map(String) : [];
96
+ const allWarnings = [...extraWarnings, ...geminiWarnings];
97
+ return {
98
+ candidateName: String(parsed.candidateName || ''),
99
+ officeSought: String(parsed.officeSought || ''),
100
+ reportPeriod: String(parsed.reportPeriod || reportName),
101
+ beginningBalance: Number(parsed.beginningBalance) || 0,
102
+ totalContributions: Number(parsed.totalContributions) || 0,
103
+ totalExpenditures: Number(parsed.totalExpenditures) || 0,
104
+ endingBalance: Number(parsed.endingBalance) || 0,
105
+ contributions: Array.isArray(parsed.contributions)
106
+ ? parsed.contributions.map(c => ({
107
+ date: String(c.date || ''),
108
+ name: String(c.name || ''),
109
+ address: String(c.address || ''),
110
+ amount: Number(c.amount) || 0,
111
+ type: String(c.type || 'Monetary'),
112
+ employer: c.employer ? String(c.employer) : undefined,
113
+ occupation: c.occupation ? String(c.occupation) : undefined,
114
+ }))
115
+ : [],
116
+ expenditures: Array.isArray(parsed.expenditures)
117
+ ? parsed.expenditures.map(e => ({
118
+ date: String(e.date || ''),
119
+ payee: String(e.payee || ''),
120
+ address: String(e.address || ''),
121
+ amount: Number(e.amount) || 0,
122
+ purpose: e.purpose ? String(e.purpose) : undefined,
123
+ }))
124
+ : [],
125
+ extractionModel: model,
126
+ extractionConfidence: determineConfidence(parsed, allWarnings),
127
+ warnings: allWarnings,
128
+ };
129
+ }
130
+ // ============================================================
131
+ // Primary: Gemini Flash extraction
132
+ // ============================================================
133
+ async function extractViaGemini(pdfBuffer, reportName) {
134
+ const base64Pdf = pdfBuffer.toString('base64');
135
+ const requestBody = {
136
+ contents: [{
137
+ parts: [
138
+ { text: EXTRACTION_PROMPT },
139
+ { inlineData: { mimeType: 'application/pdf', data: base64Pdf } },
140
+ ],
141
+ }],
142
+ generationConfig: {
143
+ temperature: 0.1,
144
+ maxOutputTokens: 65536,
145
+ thinkingConfig: { thinkingBudget: 0 },
146
+ },
147
+ };
148
+ let retried = false;
149
+ const doFetch = async () => {
150
+ const response = await fetch(GEMINI_URL, {
151
+ method: 'POST',
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ 'x-goog-api-key': getApiKey(),
155
+ },
156
+ body: JSON.stringify(requestBody),
157
+ });
158
+ if (!response.ok) {
159
+ const status = response.status;
160
+ if ((status === 429 || status >= 500) && !retried) {
161
+ retried = true;
162
+ await new Promise(r => setTimeout(r, 2000));
163
+ return doFetch();
164
+ }
165
+ const errorText = await response.text();
166
+ throw new Error(`Gemini API HTTP ${status}: ${errorText.slice(0, 500)}`);
167
+ }
168
+ return await response.json();
169
+ };
170
+ const geminiResult = await doFetch();
171
+ const { text: rawText, finishReason } = parseGeminiResponse(geminiResult);
172
+ const cleanedJson = cleanJsonResponse(rawText);
173
+ const warnings = [];
174
+ if (finishReason === 'MAX_TOKENS') {
175
+ warnings.push('Response was truncated (MAX_TOKENS). Some data may be missing.');
176
+ }
177
+ let parsed;
178
+ try {
179
+ parsed = JSON.parse(cleanedJson);
180
+ }
181
+ catch {
182
+ throw new Error(`Failed to parse Gemini JSON. finishReason: ${finishReason}. First 500 chars: ${rawText.slice(0, 500)}`);
183
+ }
184
+ if (parsed.error) {
185
+ throw new Error(`Gemini extraction: ${parsed.error}`);
186
+ }
187
+ return buildReport(parsed, reportName, GEMINI_MODEL, warnings);
188
+ }
189
+ const execFileAsync = promisify(execFile);
190
+ async function convertPdfToImages(pdfBuffer, maxPages = 10) {
191
+ const tmpDir = join(tmpdir(), `tn-elections-pdf-${Date.now()}`);
192
+ mkdirSync(tmpDir, { recursive: true });
193
+ const pdfPath = join(tmpDir, 'report.pdf');
194
+ writeFileSync(pdfPath, pdfBuffer);
195
+ try {
196
+ // Convert PDF pages to JPEG (async, non-blocking)
197
+ await execFileAsync('pdftoppm', [
198
+ '-f', '1', '-l', String(maxPages),
199
+ '-jpeg', '-jpegopt', 'quality=60', '-r', '200',
200
+ pdfPath, join(tmpDir, 'page'),
201
+ ], { timeout: 30_000 });
202
+ const images = [];
203
+ for (let i = 1; i <= maxPages; i++) {
204
+ // pdftoppm names files page-01.jpg, page-02.jpg, etc.
205
+ const padded = String(i).padStart(String(maxPages).length, '0');
206
+ const imgPath = join(tmpDir, `page-${padded}.jpg`);
207
+ if (existsSync(imgPath)) {
208
+ const imgBuffer = readFileSync(imgPath);
209
+ images.push({
210
+ page: i,
211
+ base64: imgBuffer.toString('base64'),
212
+ mimeType: 'image/jpeg',
213
+ });
214
+ }
215
+ }
216
+ return {
217
+ type: 'image_fallback',
218
+ pageCount: images.length,
219
+ images,
220
+ message: `Gemini unavailable. Converted ${images.length} PDF page(s) to images. Please read these images to extract the campaign finance data.`,
221
+ };
222
+ }
223
+ finally {
224
+ try {
225
+ rmSync(tmpDir, { recursive: true, force: true });
226
+ }
227
+ catch { /* ignore cleanup errors */ }
228
+ }
229
+ }
230
+ // ============================================================
231
+ // Public API
232
+ // ============================================================
233
+ export async function extractReportFromPdf(pdfUrl, reportName) {
234
+ // Check cache
235
+ const cached = reportCache.get(pdfUrl);
236
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
237
+ return cached.data;
238
+ }
239
+ const pdfBuffer = await fetchPdf(pdfUrl);
240
+ // Try Gemini first
241
+ const apiKey = getApiKey();
242
+ if (apiKey) {
243
+ try {
244
+ const report = await extractViaGemini(pdfBuffer, reportName);
245
+ reportCache.set(pdfUrl, { data: report, timestamp: Date.now() });
246
+ return report;
247
+ }
248
+ catch (error) {
249
+ // Gemini failed — fall through to image fallback
250
+ const msg = error instanceof Error ? error.message : String(error);
251
+ console.error(`Gemini extraction failed, falling back to images: ${msg}`);
252
+ }
253
+ }
254
+ // Fallback: convert to images for Claude to read
255
+ return await convertPdfToImages(pdfBuffer);
256
+ }
257
+ /** Check if a result is the image fallback (not structured data) */
258
+ export function isImageFallback(result) {
259
+ return 'type' in result && result.type === 'image_fallback';
260
+ }
261
+ //# sourceMappingURL=gemini-pdf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gemini-pdf.js","sourceRoot":"","sources":["../../src/api/gemini-pdf.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * TN Secretary of State candidate filings client.
3
+ * Fetches xlsx files from sos.tn.gov and parses them into typed candidate data.
4
+ * No auth, no sessions — just static page scraping + xlsx download.
5
+ */
6
+ import type { SosCandidateIndex } from '../types.js';
7
+ /**
8
+ * Fetch and parse all SOS candidate filing data for a given election year.
9
+ *
10
+ * 1. Fetches the HTML page listing xlsx/pdf download links
11
+ * 2. Downloads all xlsx files in parallel
12
+ * 3. Parses each into typed candidate records
13
+ * 4. Caches the combined result for 15 minutes
14
+ */
15
+ export declare function getFiledCandidates(year?: number): Promise<SosCandidateIndex>;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * TN Secretary of State candidate filings client.
3
+ * Fetches xlsx files from sos.tn.gov and parses them into typed candidate data.
4
+ * No auth, no sessions — just static page scraping + xlsx download.
5
+ */
6
+ import { parseCandidateListPage, parseXlsx } from '../parsers/sos-xlsx.js';
7
+ const TIMEOUT_MS = 15_000;
8
+ const CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes
9
+ const cache = new Map();
10
+ function buildPageUrl(year) {
11
+ return `https://sos.tn.gov/elections/${year}-candidate-lists`;
12
+ }
13
+ async function fetchWithTimeout(url) {
14
+ const controller = new AbortController();
15
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
16
+ try {
17
+ const response = await fetch(url, { signal: controller.signal });
18
+ if (!response.ok) {
19
+ throw new Error(`HTTP ${response.status} fetching ${url}`);
20
+ }
21
+ return response;
22
+ }
23
+ finally {
24
+ clearTimeout(timeout);
25
+ }
26
+ }
27
+ /**
28
+ * Fetch and parse all SOS candidate filing data for a given election year.
29
+ *
30
+ * 1. Fetches the HTML page listing xlsx/pdf download links
31
+ * 2. Downloads all xlsx files in parallel
32
+ * 3. Parses each into typed candidate records
33
+ * 4. Caches the combined result for 15 minutes
34
+ */
35
+ export async function getFiledCandidates(year = 2026) {
36
+ // Check cache
37
+ const cached = cache.get(year);
38
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
39
+ return cached;
40
+ }
41
+ // Fetch the HTML page
42
+ const pageUrl = buildPageUrl(year);
43
+ const pageResponse = await fetchWithTimeout(pageUrl);
44
+ const html = await pageResponse.text();
45
+ // Parse office categories with xlsx/pdf URLs
46
+ const offices = parseCandidateListPage(html);
47
+ if (offices.length === 0) {
48
+ throw new Error(`No candidate list files found at ${pageUrl}. The page may have changed structure or the year may not be available.`);
49
+ }
50
+ // Download all xlsx files in parallel (small files, ~10-18KB each)
51
+ const xlsxResults = await Promise.all(offices.map(async (office) => {
52
+ const response = await fetchWithTimeout(office.xlsxUrl);
53
+ const buffer = await response.arrayBuffer();
54
+ return { office, buffer };
55
+ }));
56
+ // Parse each xlsx file
57
+ const allCandidates = xlsxResults.flatMap(({ office, buffer }) => {
58
+ const candidates = parseXlsx(buffer, office.name, year);
59
+ office.candidateCount = candidates.length;
60
+ return candidates;
61
+ });
62
+ const index = {
63
+ year,
64
+ offices,
65
+ candidates: allCandidates,
66
+ fetchedAt: Date.now(),
67
+ };
68
+ // Cache (don't cache empty results — might be a fetch issue)
69
+ if (allCandidates.length > 0) {
70
+ cache.set(year, index);
71
+ }
72
+ return index;
73
+ }
74
+ //# sourceMappingURL=sos-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sos-client.js","sourceRoot":"","sources":["../../src/api/sos-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Sumner County campaign finance client.
3
+ * Scrapes static HTML pages from votesumnertn.org.
4
+ * No auth, no session management, UTF-8.
5
+ */
6
+ import type { SumnerCyclePage, SumnerElectionCycle, SumnerReportData } from '../types.js';
7
+ import type { PdfImageFallback } from './gemini-pdf.js';
8
+ import { resolveCycle } from '../data/sumner-cycles.js';
9
+ export declare function listCycles(): SumnerElectionCycle[];
10
+ export { resolveCycle };
11
+ export declare function getCycleCandidates(cycleInput: string): Promise<SumnerCyclePage>;
12
+ export declare function getReport(pdfUrl: string, reportName: string): Promise<SumnerReportData | PdfImageFallback>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Sumner County campaign finance client.
3
+ * Scrapes static HTML pages from votesumnertn.org.
4
+ * No auth, no session management, UTF-8.
5
+ */
6
+ import { SUMNER_CYCLES, resolveCycle } from '../data/sumner-cycles.js';
7
+ import { parseSumnerCyclePage } from '../parsers/sumner-html.js';
8
+ import { extractReportFromPdf } from './gemini-pdf.js';
9
+ const TIMEOUT_MS = 15_000;
10
+ const cyclePageCache = new Map();
11
+ const CACHE_TTL_MS = 15 * 60 * 1000;
12
+ async function fetchPage(url) {
13
+ const controller = new AbortController();
14
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
15
+ try {
16
+ const response = await fetch(url, {
17
+ signal: controller.signal,
18
+ headers: { 'Accept': 'text/html' },
19
+ });
20
+ if (!response.ok) {
21
+ throw new Error(`HTTP ${response.status} fetching ${url}`);
22
+ }
23
+ return await response.text();
24
+ }
25
+ finally {
26
+ clearTimeout(timeout);
27
+ }
28
+ }
29
+ export function listCycles() {
30
+ return SUMNER_CYCLES;
31
+ }
32
+ export { resolveCycle };
33
+ export async function getCycleCandidates(cycleInput) {
34
+ const cycle = resolveCycle(cycleInput);
35
+ if (!cycle) {
36
+ const available = SUMNER_CYCLES.map(c => c.year).join(', ');
37
+ throw new Error(`Cycle "${cycleInput}" not found. Available: ${available}`);
38
+ }
39
+ const cached = cyclePageCache.get(cycle.year);
40
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
41
+ return cached.data;
42
+ }
43
+ const html = await fetchPage(cycle.url);
44
+ const result = parseSumnerCyclePage(html, cycle);
45
+ if (result.candidates.length === 0) {
46
+ result.candidates = []; // ensure empty array not undefined
47
+ // Don't cache empty results — might be a parsing issue
48
+ }
49
+ else {
50
+ cyclePageCache.set(cycle.year, { data: result, timestamp: Date.now() });
51
+ }
52
+ return result;
53
+ }
54
+ export async function getReport(pdfUrl, reportName) {
55
+ return extractReportFromPdf(pdfUrl, reportName);
56
+ }
57
+ //# sourceMappingURL=sumner-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sumner-client.js","sourceRoot":"","sources":["../../src/api/sumner-client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAEtD,MAAM,UAAU,GAAG,MAAM,CAAA;AACzB,MAAM,cAAc,GAAG,IAAI,GAAG,EAAwD,CAAA;AACtF,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAEnC,KAAK,UAAU,SAAS,CAAC,GAAW;IAClC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;IACxC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,UAAU,CAAC,CAAA;IAChE,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,OAAO,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE;SACnC,CAAC,CAAA;QACF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC,CAAA;QAC5D,CAAC;QACD,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;IAC9B,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,OAAO,CAAC,CAAA;IACvB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,aAAa,CAAA;AACtB,CAAC;AAED,OAAO,EAAE,YAAY,EAAE,CAAA;AAEvB,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,UAAkB;IACzD,MAAM,KAAK,GAAG,YAAY,CAAC,UAAU,CAAC,CAAA;IACtC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC3D,MAAM,IAAI,KAAK,CAAC,UAAU,UAAU,2BAA2B,SAAS,EAAE,CAAC,CAAA;IAC7E,CAAC;IAED,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC7C,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,YAAY,EAAE,CAAC;QAC3D,OAAO,MAAM,CAAC,IAAI,CAAA;IACpB,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACvC,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAEhD,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,CAAC,UAAU,GAAG,EAAE,CAAA,CAAC,mCAAmC;QAC1D,uDAAuD;IACzD,CAAC;SAAM,CAAC;QACN,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IACzE,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAc,EAAE,UAAkB;IAChE,OAAO,oBAAoB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;AACjD,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * tncamp (Tennessee Registry of Election Finance) HTTP client.
3
+ *
4
+ * Session-based Java web app at apps.tn.gov/tncamp/public/.
5
+ * Flow: GET form → POST → 302 redirect → GET results page.
6
+ * Responses are Windows-1252 encoded.
7
+ */
8
+ import type { TncampCandidate, TncampReport, TncampReportDetail, TncampContribution, TncampExpenditure, TncampFormOptions, TncampDistrict, TncampOffice, TncampElectionYear } from '../types.js';
9
+ export declare function getFormOptions(): Promise<TncampFormOptions>;
10
+ export declare function getDistricts(officeId: string): Promise<TncampDistrict[]>;
11
+ export declare function resolveOffice(nameOrId: string): Promise<TncampOffice | null>;
12
+ export declare function resolveDistrict(officeId: string, labelOrId: string): Promise<TncampDistrict | null>;
13
+ export declare function resolveElectionYear(yearStr: string): Promise<TncampElectionYear | null>;
14
+ export declare function resolveParty(nameOrId: string): Promise<{
15
+ id: string;
16
+ name: string;
17
+ } | null>;
18
+ export interface CandidateSearchParams {
19
+ name?: string;
20
+ searchType?: 'candidate' | 'pac' | 'both';
21
+ officeId?: string;
22
+ district?: string;
23
+ electionYearId?: string;
24
+ partyId?: string;
25
+ }
26
+ export declare function searchCandidates(params: CandidateSearchParams): Promise<TncampCandidate[]>;
27
+ export declare function getCandidateReports(candidateId: number, ownerName: string): Promise<TncampReport[]>;
28
+ export declare function getReportDetail(reportId: number): Promise<TncampReportDetail>;
29
+ export interface ContributionSearchParams {
30
+ candName?: string;
31
+ contributorName?: string;
32
+ employer?: string;
33
+ occupation?: string;
34
+ zipCode?: string;
35
+ typeOf?: 'monetary' | 'inkind' | 'independent' | 'all';
36
+ amountDollars?: number;
37
+ amountSelection?: 'equal' | 'greater' | 'less';
38
+ year: string;
39
+ electionYearId?: string;
40
+ }
41
+ export interface ExpenditureSearchParams {
42
+ candName?: string;
43
+ vendorName?: string;
44
+ vendorZipCode?: string;
45
+ purpose?: string;
46
+ typeOf?: 'monetary' | 'inkind' | 'independent' | 'all';
47
+ amountDollars?: number;
48
+ amountSelection?: 'equal' | 'greater' | 'less';
49
+ year: string;
50
+ electionYearId?: string;
51
+ }
52
+ export declare function searchContributions(params: ContributionSearchParams): Promise<{
53
+ items: TncampContribution[];
54
+ totalCount: number;
55
+ truncated: boolean;
56
+ }>;
57
+ export declare function searchExpenditures(params: ExpenditureSearchParams): Promise<{
58
+ items: TncampExpenditure[];
59
+ totalCount: number;
60
+ truncated: boolean;
61
+ }>;