@cyanheads/congressgov-mcp-server 0.3.18 → 0.3.20

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 (59) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/README.md +5 -3
  4. package/dist/mcp-server/tools/definitions/bill-lookup.tool.d.ts +12 -1
  5. package/dist/mcp-server/tools/definitions/bill-lookup.tool.d.ts.map +1 -1
  6. package/dist/mcp-server/tools/definitions/bill-lookup.tool.js +28 -9
  7. package/dist/mcp-server/tools/definitions/bill-lookup.tool.js.map +1 -1
  8. package/dist/mcp-server/tools/definitions/bill-summaries.tool.d.ts +8 -1
  9. package/dist/mcp-server/tools/definitions/bill-summaries.tool.d.ts.map +1 -1
  10. package/dist/mcp-server/tools/definitions/bill-summaries.tool.js +13 -5
  11. package/dist/mcp-server/tools/definitions/bill-summaries.tool.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/committee-lookup.tool.d.ts +8 -1
  13. package/dist/mcp-server/tools/definitions/committee-lookup.tool.d.ts.map +1 -1
  14. package/dist/mcp-server/tools/definitions/committee-lookup.tool.js +37 -12
  15. package/dist/mcp-server/tools/definitions/committee-lookup.tool.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/committee-reports.tool.d.ts +8 -1
  17. package/dist/mcp-server/tools/definitions/committee-reports.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/committee-reports.tool.js +9 -2
  19. package/dist/mcp-server/tools/definitions/committee-reports.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/definitions/crs-reports.tool.d.ts +8 -1
  21. package/dist/mcp-server/tools/definitions/crs-reports.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/crs-reports.tool.js +3 -2
  23. package/dist/mcp-server/tools/definitions/crs-reports.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/daily-record.tool.d.ts +8 -1
  25. package/dist/mcp-server/tools/definitions/daily-record.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/definitions/daily-record.tool.js +11 -4
  27. package/dist/mcp-server/tools/definitions/daily-record.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/definitions/enacted-laws.tool.d.ts +8 -1
  29. package/dist/mcp-server/tools/definitions/enacted-laws.tool.d.ts.map +1 -1
  30. package/dist/mcp-server/tools/definitions/enacted-laws.tool.js +10 -3
  31. package/dist/mcp-server/tools/definitions/enacted-laws.tool.js.map +1 -1
  32. package/dist/mcp-server/tools/definitions/member-lookup.tool.d.ts +8 -1
  33. package/dist/mcp-server/tools/definitions/member-lookup.tool.d.ts.map +1 -1
  34. package/dist/mcp-server/tools/definitions/member-lookup.tool.js +15 -3
  35. package/dist/mcp-server/tools/definitions/member-lookup.tool.js.map +1 -1
  36. package/dist/mcp-server/tools/definitions/roll-votes.tool.d.ts +12 -1
  37. package/dist/mcp-server/tools/definitions/roll-votes.tool.d.ts.map +1 -1
  38. package/dist/mcp-server/tools/definitions/roll-votes.tool.js +68 -3
  39. package/dist/mcp-server/tools/definitions/roll-votes.tool.js.map +1 -1
  40. package/dist/mcp-server/tools/definitions/senate-nominations.tool.d.ts +8 -1
  41. package/dist/mcp-server/tools/definitions/senate-nominations.tool.d.ts.map +1 -1
  42. package/dist/mcp-server/tools/definitions/senate-nominations.tool.js +26 -7
  43. package/dist/mcp-server/tools/definitions/senate-nominations.tool.js.map +1 -1
  44. package/dist/mcp-server/tools/format-helpers.d.ts +3 -3
  45. package/dist/mcp-server/tools/format-helpers.d.ts.map +1 -1
  46. package/dist/mcp-server/tools/format-helpers.js +657 -42
  47. package/dist/mcp-server/tools/format-helpers.js.map +1 -1
  48. package/dist/mcp-server/tools/tool-helpers.d.ts +34 -0
  49. package/dist/mcp-server/tools/tool-helpers.d.ts.map +1 -1
  50. package/dist/mcp-server/tools/tool-helpers.js +96 -0
  51. package/dist/mcp-server/tools/tool-helpers.js.map +1 -1
  52. package/dist/services/congress-api/congress-api-service.d.ts +1 -1
  53. package/dist/services/congress-api/congress-api-service.d.ts.map +1 -1
  54. package/dist/services/congress-api/congress-api-service.js +69 -26
  55. package/dist/services/congress-api/congress-api-service.js.map +1 -1
  56. package/dist/services/congress-api/types.d.ts +1 -0
  57. package/dist/services/congress-api/types.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/server.json +3 -3
@@ -14,16 +14,31 @@
14
14
  function tb(content) {
15
15
  return [{ type: 'text', text: content }];
16
16
  }
17
- function stripHtml(html) {
18
- return html
17
+ /**
18
+ * Strip HTML to plain text while preserving paragraph and line breaks. Upstream
19
+ * summary fields and other narrative bodies ship as HTML; we want the visible
20
+ * structure (paragraph boundaries) to survive into the rendered Markdown.
21
+ *
22
+ * Inline contexts that need single-line output should pass `{ inline: true }`.
23
+ */
24
+ function stripHtml(html, { inline = false } = {}) {
25
+ const text = html
26
+ .replace(/<\s*br\s*\/?\s*>/gi, '\n')
27
+ .replace(/<\s*\/p\s*>/gi, '\n\n')
19
28
  .replace(/<[^>]*>/g, '')
20
29
  .replace(/&nbsp;/g, ' ')
21
30
  .replace(/&#039;/g, "'")
22
31
  .replace(/&amp;/g, '&')
23
32
  .replace(/&lt;/g, '<')
24
33
  .replace(/&gt;/g, '>')
25
- .replace(/&quot;/g, '"')
26
- .replace(/\s+/g, ' ')
34
+ .replace(/&quot;/g, '"');
35
+ if (inline)
36
+ return text.replace(/\s+/g, ' ').trim();
37
+ return text
38
+ .replace(/[ \t]+/g, ' ')
39
+ .replace(/\n[ \t]+/g, '\n')
40
+ .replace(/[ \t]+\n/g, '\n')
41
+ .replace(/\n{3,}/g, '\n\n')
27
42
  .trim();
28
43
  }
29
44
  /**
@@ -54,7 +69,7 @@ function htmlToMarkdown(html) {
54
69
  .replace(/\n{3,}/g, '\n\n')
55
70
  .trim();
56
71
  }
57
- /** Safe deep access with HTML stripping. Handles string and number values. */
72
+ /** Safe deep access for compact field display collapses whitespace to a single line. */
58
73
  function s(obj, ...path) {
59
74
  let cur = obj;
60
75
  for (const key of path) {
@@ -63,7 +78,7 @@ function s(obj, ...path) {
63
78
  cur = cur[key];
64
79
  }
65
80
  if (typeof cur === 'string')
66
- return stripHtml(cur);
81
+ return stripHtml(cur, { inline: true });
67
82
  if (typeof cur === 'number')
68
83
  return String(cur);
69
84
  return;
@@ -85,17 +100,38 @@ function pagHeader(result) {
85
100
  return `**${count} result${count !== 1 ? 's' : ''}**${next != null ? ` | next offset: ${next}` : ''}`;
86
101
  }
87
102
  /** Render a paginated list with header and per-item rendering. */
88
- function renderList(result, renderItem) {
103
+ function renderList(result, renderItem, query) {
89
104
  const items = (result.data ?? []);
90
105
  const header = pagHeader(result);
106
+ const queryLine = query ? `_Search: ${query}_` : undefined;
91
107
  if (items.length === 0) {
92
- return `${header}\n\nNo results matched the query. Try broadening the filterswiden the date range, drop the type/chamber/state constraint, or remove the date window entirely.`;
108
+ /** Distinguish "0 total" from "page is past the end" — the header alone reads
109
+ * as "N items returned but didn't render" when count > 0 and the page is empty. */
110
+ const p = result.pagination;
111
+ const total = p?.count ?? 0;
112
+ const pageHint = total > 0
113
+ ? `_Page is empty — offset is past the end of ${total} total item${total !== 1 ? 's' : ''}._`
114
+ : query
115
+ ? '_No matching results. Broaden the filter or check the criteria above._'
116
+ : '_No matching results._';
117
+ return [queryLine, header, '', pageHint].filter(Boolean).join('\n\n');
93
118
  }
94
119
  const renderer = renderItem ?? renderGenericItem;
95
120
  const rendered = items.map((item, i) => typeof item === 'object' && item !== null
96
121
  ? renderer(item, i)
97
122
  : `${i + 1}. ${String(item)}`);
98
- return [header, '', ...rendered].join('\n\n');
123
+ return [queryLine, header, '', ...rendered].filter(Boolean).join('\n\n');
124
+ }
125
+ /**
126
+ * Render an object's remaining fields after a hand-built header. Skips keys
127
+ * already consumed by the header so they don't repeat in the body.
128
+ */
129
+ function renderDetailRest(obj, skip) {
130
+ const filtered = {};
131
+ for (const [k, v] of Object.entries(obj))
132
+ if (!skip.has(k))
133
+ filtered[k] = v;
134
+ return renderDetail(filtered);
99
135
  }
100
136
  /** Render any object as structured markdown. Used for detail views. */
101
137
  function renderDetail(obj) {
@@ -118,13 +154,13 @@ function renderDetail(obj) {
118
154
  if (val == null || val === '')
119
155
  continue;
120
156
  if (typeof val === 'string') {
121
- const cleaned = stripHtml(val);
122
- if (cleaned.length > 300) {
157
+ const inline = stripHtml(val, { inline: true });
158
+ if (inline.length > 300) {
123
159
  lines.push(`**${key}:**`);
124
- lines.push(cleaned);
160
+ lines.push(stripHtml(val));
125
161
  }
126
162
  else {
127
- lines.push(`**${key}:** ${cleaned}`);
163
+ lines.push(`**${key}:** ${inline}`);
128
164
  }
129
165
  }
130
166
  else if (typeof val === 'number' || typeof val === 'boolean') {
@@ -172,7 +208,7 @@ function renderDetail(obj) {
172
208
  if (v2 == null || v2 === '')
173
209
  continue;
174
210
  if (typeof v2 === 'string')
175
- lines.push(` **${k2}:** ${stripHtml(v2)}`);
211
+ lines.push(` **${k2}:** ${stripHtml(v2, { inline: true })}`);
176
212
  else if (typeof v2 === 'number' || typeof v2 === 'boolean')
177
213
  lines.push(` **${k2}:** ${v2}`);
178
214
  else if (typeof v2 === 'object' && v2)
@@ -203,13 +239,13 @@ function renderGenericItem(item, index) {
203
239
  if (HEADING_FIELDS.has(key))
204
240
  continue;
205
241
  if (typeof val === 'string') {
206
- const cleaned = stripHtml(val);
207
- if (cleaned.length > 300) {
242
+ const inline = stripHtml(val, { inline: true });
243
+ if (inline.length > 300) {
208
244
  lines.push(`**${key}:**`);
209
- lines.push(cleaned);
245
+ lines.push(stripHtml(val));
210
246
  }
211
247
  else {
212
- lines.push(`**${key}:** ${cleaned}`);
248
+ lines.push(`**${key}:** ${inline}`);
213
249
  }
214
250
  }
215
251
  else if (typeof val === 'number' || typeof val === 'boolean') {
@@ -262,7 +298,7 @@ function renderInline(obj) {
262
298
  if (val == null || val === '')
263
299
  continue;
264
300
  if (typeof val === 'string') {
265
- const cleaned = stripHtml(val);
301
+ const cleaned = stripHtml(val, { inline: true });
266
302
  const preview = cleaned.length > 120 ? `${cleaned.slice(0, 117)}...` : cleaned;
267
303
  parts.push(`${key}: ${preview}`);
268
304
  }
@@ -390,7 +426,8 @@ function renderCrsReportItem(item, i) {
390
426
  ]);
391
427
  if (meta)
392
428
  lines.push(meta);
393
- lines.push(summary || '_Summary not available._');
429
+ if (summary)
430
+ lines.push(summary);
394
431
  if (url)
395
432
  lines.push(`**URL:** ${url}`);
396
433
  return lines.join('\n');
@@ -522,14 +559,533 @@ function renderCommitteeReportTextItem(item, i) {
522
559
  }
523
560
  return lines.join('\n');
524
561
  }
562
+ /** Member-sponsored amendments — `type`/`title` are null upstream; identify by `amendmentNumber`. */
563
+ function renderAmendmentItem(item, i) {
564
+ const number = s(item, 'amendmentNumber');
565
+ const url = s(item, 'url') ?? '';
566
+ /** URL path carries the chamber prefix (samdt / hamdt) we need for a readable type label. */
567
+ const amdMatch = url.match(/\/amendment\/(\d+)\/(samdt|hamdt|suamdt|huamdt)\//i);
568
+ const typeCode = amdMatch?.[2]?.toLowerCase();
569
+ const chamber = typeCode === 'samdt' || typeCode === 'suamdt'
570
+ ? 'Senate Amendment'
571
+ : typeCode === 'hamdt' || typeCode === 'huamdt'
572
+ ? 'House Amendment'
573
+ : 'Amendment';
574
+ const heading = number ? `${chamber} ${number}` : 'Amendment';
575
+ const lines = [`### ${i + 1}. ${heading}`];
576
+ const meta = join([
577
+ f('Congress', s(item, 'congress')),
578
+ f('Introduced', s(item, 'introducedDate')),
579
+ ]);
580
+ if (meta)
581
+ lines.push(meta);
582
+ const actionDate = s(item, 'latestAction', 'actionDate');
583
+ const actionText = s(item, 'latestAction', 'text');
584
+ if (actionDate || actionText)
585
+ lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
586
+ if (url)
587
+ lines.push(`**URL:** ${url}`);
588
+ return lines.join('\n');
589
+ }
590
+ /** Bill text versions — heading from `type` (e.g. "Enrolled Bill"), formats[] as labeled URLs. */
591
+ function renderBillTextItem(item, i) {
592
+ const type = s(item, 'type') ?? 'Bill Text';
593
+ const date = s(item, 'date');
594
+ const lines = [`### ${i + 1}. ${type}`];
595
+ if (date)
596
+ lines.push(`**Date:** ${date}`);
597
+ const formats = item.formats;
598
+ if (Array.isArray(formats)) {
599
+ for (const fmt of formats) {
600
+ const fType = s(fmt, 'type');
601
+ const fUrl = s(fmt, 'url');
602
+ if (fType && fUrl)
603
+ lines.push(`**${fType}:** ${fUrl}`);
604
+ }
605
+ }
606
+ return lines.join('\n');
607
+ }
608
+ /** Nomination type wrapper: `{isCivilian: true}` / `{isMilitary: true}` → readable label. */
609
+ function nominationTypeLabel(raw) {
610
+ if (!raw || typeof raw !== 'object')
611
+ return;
612
+ const t = raw;
613
+ if (t.isCivilian === true)
614
+ return 'Civilian';
615
+ if (t.isMilitary === true)
616
+ return 'Military';
617
+ return;
618
+ }
619
+ function nominationHeading(item) {
620
+ const citation = s(item, 'citation');
621
+ if (citation)
622
+ return citation;
623
+ const number = s(item, 'number');
624
+ const partNumber = s(item, 'partNumber');
625
+ if (number && partNumber && partNumber !== '00')
626
+ return `PN${number}-${Number(partNumber)}`;
627
+ if (number)
628
+ return `PN${number}`;
629
+ return 'Nomination';
630
+ }
631
+ function renderNominationListItem(item, i) {
632
+ const heading = nominationHeading(item);
633
+ const type = nominationTypeLabel(item.nominationType);
634
+ const lines = [`### ${i + 1}. ${heading}`];
635
+ const description = s(item, 'description');
636
+ if (description)
637
+ lines.push(description);
638
+ const meta = join([
639
+ f('Congress', s(item, 'congress')),
640
+ f('Type', type),
641
+ f('Received', s(item, 'receivedDate')),
642
+ f('Authority Date', s(item, 'authorityDate')),
643
+ f('Updated', s(item, 'updateDate')),
644
+ ]);
645
+ if (meta)
646
+ lines.push(meta);
647
+ const actionDate = s(item, 'latestAction', 'actionDate');
648
+ const actionText = s(item, 'latestAction', 'text');
649
+ if (actionDate || actionText)
650
+ lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
651
+ const url = s(item, 'url');
652
+ if (url)
653
+ lines.push(`**URL:** ${url}`);
654
+ return lines.join('\n');
655
+ }
656
+ /** Nomination committee items — shape {name, systemCode, chamber, type, activities[], url}. */
657
+ function renderNominationCommitteeItem(item, i) {
658
+ const name = s(item, 'name') ?? s(item, 'systemCode') ?? 'Committee';
659
+ const lines = [`### ${i + 1}. ${name}`];
660
+ const meta = join([
661
+ f('Code', s(item, 'systemCode')),
662
+ f('Chamber', s(item, 'chamber')),
663
+ f('Type', s(item, 'type')),
664
+ ]);
665
+ if (meta)
666
+ lines.push(meta);
667
+ const activities = item.activities;
668
+ if (Array.isArray(activities) && activities.length > 0) {
669
+ lines.push('**Activities:**');
670
+ for (const a of activities) {
671
+ const date = s(a, 'date')?.slice(0, 10);
672
+ const aname = s(a, 'name');
673
+ lines.push(`- ${[date, aname].filter(Boolean).join(' — ')}`);
674
+ }
675
+ }
676
+ const url = s(item, 'url');
677
+ if (url)
678
+ lines.push(`**URL:** ${url}`);
679
+ return lines.join('\n');
680
+ }
681
+ /** Individual nominee items — shape {firstName, middleName, lastName, ordinal, state, prefix?, suffix?}. */
682
+ function renderNomineeItem(item, i) {
683
+ const prefix = s(item, 'prefix');
684
+ const first = s(item, 'firstName');
685
+ const middle = s(item, 'middleName');
686
+ const last = s(item, 'lastName');
687
+ const suffix = s(item, 'suffix');
688
+ const name = [prefix, first, middle, last, suffix].filter(Boolean).join(' ').trim();
689
+ const heading = name || 'Nominee';
690
+ const lines = [`### ${i + 1}. ${heading}`];
691
+ const meta = join([f('Ordinal', s(item, 'ordinal')), f('State', s(item, 'state'))]);
692
+ if (meta)
693
+ lines.push(meta);
694
+ return lines.join('\n');
695
+ }
696
+ /** Nomination hearing items — shape {chamber, citation, date, jacketNumber, number, partNumber, errata?}. */
697
+ function renderNominationHearingItem(item, i) {
698
+ const citation = s(item, 'citation');
699
+ const number = s(item, 'number');
700
+ const heading = citation ?? (number ? `Hearing ${number}` : 'Hearing');
701
+ const lines = [`### ${i + 1}. ${heading}`];
702
+ const partNumber = s(item, 'partNumber');
703
+ const meta = join([
704
+ f('Chamber', s(item, 'chamber')),
705
+ f('Date', s(item, 'date')?.slice(0, 10)),
706
+ f('Number', number),
707
+ partNumber && partNumber !== '1' && partNumber !== '01' ? f('Part', partNumber) : undefined,
708
+ f('Jacket', s(item, 'jacketNumber')),
709
+ s(item, 'errata') === 'Y' ? '_Errata_' : undefined,
710
+ ]);
711
+ if (meta)
712
+ lines.push(meta);
713
+ return lines.join('\n');
714
+ }
715
+ /** Dispatch nomination list rows to the right renderer by shape signal. */
716
+ function pickNominationListRenderer(first) {
717
+ /** Action rows share the bill-action shape (actionDate, text, type, actionCode);
718
+ * bill-specific extensions (committees, sourceSystem) are absent and no-op. */
719
+ if ('actionDate' in first && 'text' in first)
720
+ return renderBillActionItem;
721
+ /** Nominee rows: firstName/lastName, or ordinal + state without citation. */
722
+ if ('firstName' in first || 'lastName' in first)
723
+ return renderNomineeItem;
724
+ /** Committee rows: systemCode + name. */
725
+ if ('systemCode' in first && 'name' in first)
726
+ return renderNominationCommitteeItem;
727
+ /** Hearing rows: jacketNumber is unique to hearings. */
728
+ if ('jacketNumber' in first)
729
+ return renderNominationHearingItem;
730
+ /** Default: nomination list rows (citation/number/partNumber + description/nominationType). */
731
+ return renderNominationListItem;
732
+ }
733
+ function renderNominationDetail(item) {
734
+ const heading = nominationHeading(item);
735
+ const type = nominationTypeLabel(item.nominationType);
736
+ const lines = [`# ${heading}`];
737
+ const description = s(item, 'description');
738
+ if (description)
739
+ lines.push(description);
740
+ const meta = join([
741
+ f('Congress', s(item, 'congress')),
742
+ f('Type', type),
743
+ f('Part Number', s(item, 'partNumber')),
744
+ f('Received', s(item, 'receivedDate')),
745
+ f('Authority Date', s(item, 'authorityDate')),
746
+ f('Updated', s(item, 'updateDate')),
747
+ ]);
748
+ if (meta)
749
+ lines.push(meta);
750
+ const actionDate = s(item, 'latestAction', 'actionDate');
751
+ const actionText = s(item, 'latestAction', 'text');
752
+ if (actionDate || actionText)
753
+ lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
754
+ const subResources = [];
755
+ for (const key of ['actions', 'committees', 'hearings']) {
756
+ const sub = item[key];
757
+ if (sub && typeof sub.count === 'number' && sub.count > 0)
758
+ subResources.push(`${sub.count} ${key}`);
759
+ }
760
+ if (subResources.length)
761
+ lines.push(`**Available:** ${subResources.join(', ')}`);
762
+ const nominees = item.nominees;
763
+ if (Array.isArray(nominees) && nominees.length > 0) {
764
+ lines.push(`\n**Nominees (${nominees.length}):**`);
765
+ for (const n of nominees.slice(0, 20)) {
766
+ const ord = s(n, 'ordinal');
767
+ const count = s(n, 'nomineeCount');
768
+ const org = s(n, 'organization');
769
+ const title = s(n, 'positionTitle');
770
+ const parts = [
771
+ ord ? `Ord ${ord}` : undefined,
772
+ count ? `${count} nominee(s)` : undefined,
773
+ org,
774
+ title,
775
+ ].filter(Boolean);
776
+ lines.push(`- ${parts.join(' — ')}`);
777
+ }
778
+ if (nominees.length > 20)
779
+ lines.push(`- _...${nominees.length - 20} more_`);
780
+ }
781
+ else if (s(item, 'partNumber') === '00') {
782
+ /** Parent nominations (partNumber=00) have no nominees array. Sub-resources
783
+ * also return 0 results — they live on the partitioned children (PN851-1, PN851-2, …). */
784
+ lines.push('\n_This is a parent nomination. Individual nominees and confirmation actions live on partitioned children (e.g. `PN851-1`, `PN851-2`). Use the partitioned form for `actions`, `committees`, `hearings`, or `nominees`._');
785
+ }
786
+ const url = s(item, 'url');
787
+ if (url)
788
+ lines.push(`\n**URL:** ${url}`);
789
+ return lines.join('\n');
790
+ }
791
+ /** Roll call vote detail — question, result, date, party totals. */
792
+ function renderRollVoteDetail(item) {
793
+ const roll = s(item, 'rollCallNumber');
794
+ const congress = s(item, 'congress');
795
+ const session = s(item, 'sessionNumber');
796
+ const result = s(item, 'result');
797
+ const question = s(item, 'voteQuestion');
798
+ const voteType = s(item, 'voteType');
799
+ const startDate = s(item, 'startDate');
800
+ const updated = s(item, 'updateDate');
801
+ const identifier = s(item, 'identifier');
802
+ const sourceUrl = s(item, 'sourceDataURL');
803
+ const headingLeft = roll ? `Roll ${roll}` : 'Roll call';
804
+ const heading = result ? `${headingLeft} — ${result}` : headingLeft;
805
+ const lines = [`# ${heading}`];
806
+ if (question)
807
+ lines.push(`**Question:** ${question}`);
808
+ const meta = join([
809
+ f('Congress', congress),
810
+ f('Session', session),
811
+ f('Type', voteType),
812
+ f('Date', startDate),
813
+ f('Updated', updated),
814
+ identifier && identifier !== roll ? f('ID', identifier) : undefined,
815
+ ]);
816
+ if (meta)
817
+ lines.push(meta);
818
+ const totals = item.votePartyTotal;
819
+ if (Array.isArray(totals) && totals.length > 0) {
820
+ lines.push('\n**Party Totals:**');
821
+ for (const t of totals) {
822
+ const party = s(t, 'party', 'name') ?? s(t, 'voteParty') ?? '?';
823
+ const yea = s(t, 'yeaTotal') ?? '0';
824
+ const nay = s(t, 'nayTotal') ?? '0';
825
+ const present = s(t, 'presentTotal') ?? '0';
826
+ const notVoting = s(t, 'notVotingTotal') ?? '0';
827
+ lines.push(`- **${party}:** Yea ${yea}, Nay ${nay}, Present ${present}, Not Voting ${notVoting}`);
828
+ }
829
+ }
830
+ const results = item.results;
831
+ if (Array.isArray(results) && results.length > 0) {
832
+ lines.push(`\n**Member Votes:** ${results.length} on this page`);
833
+ for (const r of results.slice(0, 20)) {
834
+ const name = s(r, 'firstName') && s(r, 'lastName')
835
+ ? `${s(r, 'firstName')} ${s(r, 'lastName')}`
836
+ : (s(r, 'bioguideId') ?? '?');
837
+ const vote = s(r, 'voteCast');
838
+ const party = s(r, 'voteParty');
839
+ const state = s(r, 'voteState');
840
+ const parts = [
841
+ name,
842
+ party && state ? `(${party}-${state})` : undefined,
843
+ vote ? `→ ${vote}` : undefined,
844
+ ].filter(Boolean);
845
+ lines.push(`- ${parts.join(' ')}`);
846
+ }
847
+ if (results.length > 20)
848
+ lines.push(`- _...${results.length - 20} more_`);
849
+ }
850
+ if (sourceUrl)
851
+ lines.push(`\n**Source Data URL:** ${sourceUrl}`);
852
+ return lines.join('\n');
853
+ }
854
+ /** Bill / law detail — title-first header, then the rest of the structured fields. */
855
+ function renderBillDetail(item) {
856
+ const type = s(item, 'type')?.toUpperCase() ?? '';
857
+ const number = s(item, 'number') ?? '';
858
+ const title = s(item, 'title') ?? 'Untitled';
859
+ const id = type && number ? `${type} ${number}: ` : '';
860
+ const lines = [`# ${id}${title}`];
861
+ const meta = join([
862
+ f('Congress', s(item, 'congress')),
863
+ f('Chamber', s(item, 'originChamber')),
864
+ f('Policy Area', s(item, 'policyArea', 'name')),
865
+ f('Introduced', s(item, 'introducedDate')),
866
+ f('Updated', s(item, 'updateDate')),
867
+ ]);
868
+ if (meta)
869
+ lines.push(meta);
870
+ const actionDate = s(item, 'latestAction', 'actionDate');
871
+ const actionText = s(item, 'latestAction', 'text');
872
+ if (actionDate || actionText)
873
+ lines.push(`**Latest Action:** ${[actionDate, actionText].filter(Boolean).join(' — ')}`);
874
+ const laws = item.laws;
875
+ if (Array.isArray(laws) && laws.length > 0) {
876
+ const cites = laws
877
+ .map((law) => {
878
+ const num = s(law, 'number');
879
+ const lawType = s(law, 'type');
880
+ return num && lawType ? `${lawType} ${num}` : (num ?? lawType);
881
+ })
882
+ .filter(Boolean);
883
+ if (cites.length)
884
+ lines.push(`**Law:** ${cites.join(', ')}`);
885
+ }
886
+ const rest = renderDetailRest(item, HEADER_BILL_KEYS);
887
+ if (rest)
888
+ lines.push('', rest);
889
+ return lines.join('\n');
890
+ }
891
+ const HEADER_BILL_KEYS = new Set([
892
+ 'type',
893
+ 'number',
894
+ 'title',
895
+ 'congress',
896
+ 'originChamber',
897
+ 'originChamberCode',
898
+ 'policyArea',
899
+ 'introducedDate',
900
+ 'updateDate',
901
+ 'latestAction',
902
+ 'laws',
903
+ ]);
904
+ /** CRS report detail — title-first header, then the rest of the structured fields. */
905
+ function renderCrsReportDetail(item) {
906
+ const reportNumber = s(item, 'id') ?? s(item, 'reportNumber') ?? s(item, 'number');
907
+ const title = s(item, 'title') ?? 'Title not available';
908
+ const heading = reportNumber ? `${reportNumber}: ${title}` : title;
909
+ const lines = [`# ${heading}`];
910
+ const meta = join([
911
+ f('Type', s(item, 'contentType')),
912
+ f('Status', s(item, 'status')),
913
+ f('Version', s(item, 'currentVersion') ?? s(item, 'version')),
914
+ f('Published', s(item, 'publishDate')),
915
+ f('Updated', s(item, 'updateDate')),
916
+ ]);
917
+ if (meta)
918
+ lines.push(meta);
919
+ const authors = item.authors;
920
+ if (Array.isArray(authors) && authors.length > 0) {
921
+ const names = authors
922
+ .map((a) => s(a, 'author') ?? s(a, 'name'))
923
+ .filter(Boolean);
924
+ if (names.length)
925
+ lines.push(`**Authors:** ${names.join(', ')}`);
926
+ }
927
+ const rest = renderDetailRest(item, HEADER_CRS_KEYS);
928
+ if (rest)
929
+ lines.push('', rest);
930
+ return lines.join('\n');
931
+ }
932
+ const HEADER_CRS_KEYS = new Set([
933
+ 'id',
934
+ 'reportNumber',
935
+ 'number',
936
+ 'title',
937
+ 'contentType',
938
+ 'status',
939
+ 'currentVersion',
940
+ 'version',
941
+ 'publishDate',
942
+ 'updateDate',
943
+ 'authors',
944
+ ]);
945
+ /** Member detail. */
946
+ function renderMemberDetail(item) {
947
+ const name = s(item, 'directOrderName') ??
948
+ s(item, 'invertedOrderName') ??
949
+ s(item, 'bioguideId') ??
950
+ 'Unknown';
951
+ const lines = [`# ${name}`];
952
+ const meta = join([
953
+ f('ID', s(item, 'bioguideId')),
954
+ f('Party', s(item, 'partyName') ?? s(item, 'currentParty')),
955
+ f('State', s(item, 'state')),
956
+ item.district != null ? f('District', s(item, 'district')) : undefined,
957
+ f('Currently Serving', typeof item.currentMember === 'boolean' ? String(item.currentMember) : undefined),
958
+ f('Birth Year', s(item, 'birthYear')),
959
+ f('Updated', s(item, 'updateDate')),
960
+ ]);
961
+ if (meta)
962
+ lines.push(meta);
963
+ const honorific = s(item, 'honorificName');
964
+ if (honorific)
965
+ lines.push(`**Honorific:** ${honorific}`);
966
+ /** terms may be a direct array or nested as `{item: [...]}`. */
967
+ const rawTerms = item.terms;
968
+ const termsArr = Array.isArray(rawTerms)
969
+ ? rawTerms
970
+ : rawTerms &&
971
+ typeof rawTerms === 'object' &&
972
+ Array.isArray(rawTerms.item)
973
+ ? rawTerms.item
974
+ : undefined;
975
+ if (termsArr && termsArr.length > 0) {
976
+ lines.push(`\n**Terms (${termsArr.length}):**`);
977
+ const recent = termsArr.slice(-5);
978
+ for (const term of recent) {
979
+ const chamber = s(term, 'chamber');
980
+ const start = s(term, 'startYear');
981
+ const end = s(term, 'endYear');
982
+ const party = s(term, 'partyName');
983
+ const stateName = s(term, 'stateName');
984
+ const range = start && end ? `${start}–${end}` : start;
985
+ const parts = [chamber, range, party, stateName].filter(Boolean);
986
+ lines.push(`- ${parts.join(', ')}`);
987
+ }
988
+ if (termsArr.length > 5)
989
+ lines.push(`- _...${termsArr.length - 5} earlier_`);
990
+ }
991
+ const partyHistory = item.partyHistory;
992
+ if (Array.isArray(partyHistory) && partyHistory.length > 0) {
993
+ lines.push(`\n**Party History:**`);
994
+ for (const p of partyHistory) {
995
+ const partyName = s(p, 'partyName');
996
+ const start = s(p, 'startYear');
997
+ const end = s(p, 'endYear');
998
+ const range = start && end ? `${start}–${end}` : start;
999
+ const parts = [partyName, range && `(${range})`].filter(Boolean);
1000
+ lines.push(`- ${parts.join(' ')}`);
1001
+ }
1002
+ }
1003
+ const leadership = item.leadership;
1004
+ if (Array.isArray(leadership) && leadership.length > 0) {
1005
+ lines.push(`\n**Leadership Roles (${leadership.length}):**`);
1006
+ for (const l of leadership.slice(0, 10)) {
1007
+ const type = s(l, 'type');
1008
+ const congress = s(l, 'congress');
1009
+ lines.push(`- ${[type, congress ? `Congress ${congress}` : undefined].filter(Boolean).join(' — ')}`);
1010
+ }
1011
+ if (leadership.length > 10)
1012
+ lines.push(`- _...${leadership.length - 10} more_`);
1013
+ }
1014
+ const subResources = [];
1015
+ for (const key of ['sponsoredLegislation', 'cosponsoredLegislation']) {
1016
+ const sub = item[key];
1017
+ if (sub && typeof sub.count === 'number' && sub.count > 0) {
1018
+ const label = key === 'sponsoredLegislation' ? 'sponsored' : 'cosponsored';
1019
+ subResources.push(`${sub.count} ${label}`);
1020
+ }
1021
+ }
1022
+ if (subResources.length)
1023
+ lines.push(`\n**Legislation:** ${subResources.join(', ')}`);
1024
+ const url = s(item, 'url');
1025
+ if (url)
1026
+ lines.push(`\n**URL:** ${url}`);
1027
+ return lines.join('\n');
1028
+ }
1029
+ /** Committee list item — name + key fields. */
1030
+ function renderCommitteeListItem(item, i) {
1031
+ const name = s(item, 'name') ?? s(item, 'systemCode') ?? 'Committee';
1032
+ const lines = [`### ${i + 1}. ${name}`];
1033
+ const meta = join([
1034
+ f('Code', s(item, 'systemCode')),
1035
+ f('Chamber', s(item, 'chamber')),
1036
+ f('Type', s(item, 'committeeTypeCode')),
1037
+ f('Updated', s(item, 'updateDate')),
1038
+ ]);
1039
+ if (meta)
1040
+ lines.push(meta);
1041
+ const url = s(item, 'url');
1042
+ if (url)
1043
+ lines.push(`**URL:** ${url}`);
1044
+ return lines.join('\n');
1045
+ }
1046
+ /** Committee report list item — citation-first; upstream omits title and bill ref. */
1047
+ function renderCommitteeReportListItem(item, i) {
1048
+ const citation = s(item, 'citation');
1049
+ const type = s(item, 'type');
1050
+ const number = s(item, 'number');
1051
+ const part = s(item, 'part');
1052
+ const congress = s(item, 'congress');
1053
+ const chamber = s(item, 'chamber');
1054
+ const updated = s(item, 'updateDate');
1055
+ const url = s(item, 'url');
1056
+ const heading = citation ?? (type && number ? `${type} ${congress ?? ''}-${number}` : 'Committee Report');
1057
+ const lines = [`### ${i + 1}. ${heading}`];
1058
+ const meta = join([
1059
+ f('Congress', congress),
1060
+ f('Chamber', chamber),
1061
+ f('Type', type),
1062
+ f('Number', number),
1063
+ part && part !== '1' ? f('Part', part) : undefined,
1064
+ f('Updated', updated),
1065
+ ]);
1066
+ if (meta)
1067
+ lines.push(meta);
1068
+ if (url)
1069
+ lines.push(`**URL:** ${url}`);
1070
+ return lines.join('\n');
1071
+ }
525
1072
  // ── Per-Tool Format Exports ─────────────────────────────────────────
526
- function makeFormatter(detailKeys, itemRenderer) {
1073
+ /** Read `result.query` (a string) — handlers attach this to echo filter criteria. */
1074
+ function queryOf(result) {
1075
+ return typeof result.query === 'string' ? result.query : undefined;
1076
+ }
1077
+ function makeFormatter(detailKeys, itemRenderer, detailRenderer) {
527
1078
  return (result) => {
528
1079
  if (Array.isArray(result.data))
529
- return tb(renderList(result, itemRenderer));
1080
+ return tb(renderList(result, itemRenderer, queryOf(result)));
530
1081
  for (const key of detailKeys) {
531
- if (result[key] != null)
532
- return tb(renderDetail(result[key]));
1082
+ const detail = result[key];
1083
+ if (detail != null) {
1084
+ const rendered = detailRenderer && typeof detail === 'object' && detail !== null
1085
+ ? detailRenderer(detail)
1086
+ : renderDetail(detail);
1087
+ return tb(rendered);
1088
+ }
533
1089
  }
534
1090
  return tb(renderDetail(result));
535
1091
  };
@@ -540,16 +1096,41 @@ export function formatBills(result) {
540
1096
  const first = result.data[0];
541
1097
  const firstRecord = typeof first === 'object' && first !== null ? first : undefined;
542
1098
  const renderer = firstRecord ? pickBillListRenderer(firstRecord) : undefined;
543
- return tb(renderList(result, renderer));
1099
+ return tb(renderList(result, renderer, queryOf(result)));
544
1100
  }
545
1101
  if (result.bill != null)
546
- return tb(renderDetail(result.bill));
1102
+ return tb(renderBillDetail(result.bill));
547
1103
  return tb(renderDetail(result));
548
1104
  }
1105
+ /**
1106
+ * Bill sub-resource summary item — known shape (no nested `bill.*`, since the
1107
+ * caller already has the bill). Reuses `htmlToMarkdown` so `<p>` / `<strong>`
1108
+ * survive into the rendered Markdown.
1109
+ */
1110
+ function renderBillSubresourceSummaryItem(item, i) {
1111
+ const version = s(item, 'actionDesc') ?? s(item, 'versionCode') ?? 'Summary';
1112
+ const actionDate = s(item, 'actionDate');
1113
+ const updated = s(item, 'updateDate');
1114
+ const lines = [`### ${i + 1}. ${version}`];
1115
+ const meta = join([f('Action Date', actionDate), f('Summary Updated', updated)]);
1116
+ if (meta)
1117
+ lines.push(meta);
1118
+ const text = typeof item.text === 'string' ? htmlToMarkdown(item.text) : '';
1119
+ if (text)
1120
+ lines.push('', text);
1121
+ return lines.join('\n');
1122
+ }
549
1123
  function pickBillListRenderer(first) {
550
1124
  if ('title' in first && 'number' in first)
551
1125
  return renderBillItem;
552
- // Actions always ship a `text` body; most also carry actionDate/actionCode/sourceSystem.
1126
+ /** Bill text versions: `type` + `formats[]`, no `actionDate`. */
1127
+ if ('type' in first && 'formats' in first)
1128
+ return renderBillTextItem;
1129
+ /** Bill sub-resource summaries: `actionDesc`/`versionCode` + `text`, no `actionCode`/`sourceSystem`. */
1130
+ if ('text' in first && ('actionDesc' in first || 'versionCode' in first)) {
1131
+ return renderBillSubresourceSummaryItem;
1132
+ }
1133
+ /** Actions always ship a `text` body; most also carry actionDate/actionCode/sourceSystem. */
553
1134
  if ('text' in first &&
554
1135
  ('actionDate' in first || 'actionCode' in first || 'sourceSystem' in first))
555
1136
  return renderBillActionItem;
@@ -560,16 +1141,23 @@ export const formatSummaries = makeFormatter([], renderSummaryItem);
560
1141
  /** Member browse, detail, and sponsored/cosponsored legislation. */
561
1142
  export function formatMembers(result) {
562
1143
  if (Array.isArray(result.data)) {
1144
+ const hint = queryOf(result);
563
1145
  const first = result.data[0];
564
1146
  const firstRecord = typeof first === 'object' && first !== null ? first : undefined;
565
1147
  if (firstRecord && 'bioguideId' in firstRecord)
566
- return tb(renderList(result, renderMemberItem));
567
- if (firstRecord && 'number' in firstRecord && 'title' in firstRecord)
568
- return tb(renderList(result, renderBillItem));
569
- return tb(renderList(result));
1148
+ return tb(renderList(result, renderMemberItem, hint));
1149
+ /** Sponsored/cosponsored may mix bills (type+title) and amendments (amendmentNumber, null type/title).
1150
+ * Dispatch per-row so amendments don't render as 'Untitled'. */
1151
+ if (firstRecord && ('number' in firstRecord || 'amendmentNumber' in firstRecord)) {
1152
+ const dispatch = (item, i) => 'amendmentNumber' in item && item.amendmentNumber != null
1153
+ ? renderAmendmentItem(item, i)
1154
+ : renderBillItem(item, i);
1155
+ return tb(renderList(result, dispatch, hint));
1156
+ }
1157
+ return tb(renderList(result, undefined, hint));
570
1158
  }
571
1159
  if (result.member != null)
572
- return tb(renderDetail(result.member));
1160
+ return tb(renderMemberDetail(result.member));
573
1161
  return tb(renderDetail(result));
574
1162
  }
575
1163
  /** Pull the committee's display name from nested history when the top-level `name` is missing. */
@@ -584,8 +1172,16 @@ function extractCommitteeName(committee) {
584
1172
  }
585
1173
  /** Committee browse, detail, and sub-resources (bills, reports, nominations). */
586
1174
  export function formatCommittees(result) {
587
- if (Array.isArray(result.data))
588
- return tb(renderList(result));
1175
+ if (Array.isArray(result.data)) {
1176
+ const hint = queryOf(result);
1177
+ const first = result.data[0];
1178
+ const firstRecord = typeof first === 'object' && first !== null ? first : undefined;
1179
+ /** Committee list rows have `systemCode` + `name`. Sub-resource rows
1180
+ * (bills/reports/nominations) keep their generic / specialized renderers. */
1181
+ if (firstRecord && 'systemCode' in firstRecord && 'name' in firstRecord)
1182
+ return tb(renderList(result, renderCommitteeListItem, hint));
1183
+ return tb(renderList(result, undefined, hint));
1184
+ }
589
1185
  if (result.committee != null) {
590
1186
  const committee = result.committee;
591
1187
  const name = extractCommitteeName(committee);
@@ -597,7 +1193,7 @@ export function formatCommittees(result) {
597
1193
  /** Committee reports — list, detail, and text. */
598
1194
  export function formatCommitteeReports(result) {
599
1195
  if (Array.isArray(result.data))
600
- return tb(renderList(result));
1196
+ return tb(renderList(result, renderCommitteeReportListItem, queryOf(result)));
601
1197
  if (Array.isArray(result.text)) {
602
1198
  const textResult = { data: result.text, pagination: { count: result.text.length } };
603
1199
  return tb(renderList(textResult, renderCommitteeReportTextItem));
@@ -611,9 +1207,9 @@ export function formatCommitteeReports(result) {
611
1207
  /** CRS policy analysis reports. */
612
1208
  export function formatCrsReports(result) {
613
1209
  if (Array.isArray(result.data))
614
- return tb(renderList(result, renderCrsReportItem));
1210
+ return tb(renderList(result, renderCrsReportItem, queryOf(result)));
615
1211
  if (result.report != null)
616
- return tb(renderDetail(result.report));
1212
+ return tb(renderCrsReportDetail(result.report));
617
1213
  return tb(renderDetail(result));
618
1214
  }
619
1215
  /** Daily Congressional Record — volumes, issues, articles. */
@@ -625,14 +1221,33 @@ export function formatDailyRecord(result) {
625
1221
  const renderer = firstRecord && ('sectionName' in firstRecord || 'title' in firstRecord)
626
1222
  ? renderDailyArticleItem
627
1223
  : renderDailyRecordItem;
628
- return tb(renderList(result, renderer));
1224
+ return tb(renderList(result, renderer, queryOf(result)));
629
1225
  }
630
1226
  return tb(renderDetail(result));
631
1227
  }
632
- /** Enacted public and private laws. */
633
- export const formatLaws = makeFormatter(['law'], renderBillItem);
1228
+ /** Enacted public and private laws. Upstream /law mirrors /bill, so reuse bill formatters. */
1229
+ export const formatLaws = makeFormatter(['law'], renderBillItem, renderBillDetail);
634
1230
  /** House roll call votes and member voting positions. */
635
- export const formatVotes = makeFormatter(['vote'], renderRollVoteItem);
1231
+ export function formatVotes(result) {
1232
+ if (Array.isArray(result.data))
1233
+ return tb(renderList(result, renderRollVoteItem, queryOf(result)));
1234
+ if (result.vote != null)
1235
+ return tb(renderRollVoteDetail(result.vote));
1236
+ return tb(renderDetail(result));
1237
+ }
636
1238
  /** Presidential nominations and Senate confirmation pipeline. */
637
- export const formatNominations = makeFormatter(['nomination']);
1239
+ export function formatNominations(result) {
1240
+ if (Array.isArray(result.data)) {
1241
+ const hint = queryOf(result);
1242
+ const first = result.data[0];
1243
+ const firstRecord = typeof first === 'object' && first !== null ? first : undefined;
1244
+ const renderer = firstRecord
1245
+ ? pickNominationListRenderer(firstRecord)
1246
+ : renderNominationListItem;
1247
+ return tb(renderList(result, renderer, hint));
1248
+ }
1249
+ if (result.nomination != null)
1250
+ return tb(renderNominationDetail(result.nomination));
1251
+ return tb(renderDetail(result));
1252
+ }
638
1253
  //# sourceMappingURL=format-helpers.js.map