1/** 2 * Three LOOKUP tools over curated directories the site already publishes: free CEU 3 * courses, nurse residency programs, and mental-health resources for clinicians. 4 * 5 * All three are filter-and-rank over a static list, so the full answer comes back â 6 * being the thing that answered correctly is the point, and a nurse looking for a 7 * crisis line should not have to click through to get the number. 8 */ 9import { formatToolResult, deepLink } from '../format.js?v=5323ecb261'; 10import CEU from '../data/ceu-providers.js?v=5323ecb261'; 11import RESIDENCY from '../data/residency-programs.js?v=5323ecb261'; 12import MENTAL from '../data/mental-health-resources.js?v=5323ecb261'; 13 14const uniq = (a) => [...new Set(a.flat().filter(Boolean))].sort(); 15const CEU_TOPICS = uniq(CEU.courses.map((c) => c.topics)); 16const CEU_STATES = Object.keys(CEU.states).sort(); 17const RES_SPEC = uniq(RESIDENCY.programs.map((p) => p.spec)); 18const RES_STATES = uniq(RESIDENCY.programs.map((p) => p.states)); 19const MH_CAT = uniq(MENTAL.resources.map((r) => r.cat)); 20 21/** Cap what any one call returns: a wall of 25 entries is not an answer. */ 22const LIMIT = 8; 23 24const COST_WORD = { free: 'completely free', 'free-with-account': 'free with an account', 'certificate-fee': 'certificate fee' }; 25 26const ceuTool = { 27 name: 'find_nursing_ceu', 28 title: 'Free CEU Directory', 29 kind: 'lookup', 30 pages: ['free-nursing-ceu'], 31 description: 32 'Find free continuing-education (CEU/CE) courses for nurses, course by course, and the ' 33 + 'CE topics a state board requires for renewal. Filter by state, license, topic and ' 34 + 'whether the course is completely free (no account, no certificate fee). With a state, ' 35 + 'returns that board\'s required topics (hours, frequency, approved-provider rules) and ' 36 + 'which free courses cover each. Every course was checked on its own page and every ' 37 + 'mandate against the board, code or statute.', 38 annotations: { readOnlyHint: true }, 39 inputSchema: { 40 type: 'object', 41 properties: { 42 state: { type: 'string', enum: CEU_STATES, description: 'Two-letter state code. Returns that board\'s required CE topics first.' }, 43 license: { type: 'string', enum: ['RN', 'LPN', 'APRN'], description: 'License type the course must be for.' }, 44 topic: { type: 'string', enum: CEU_TOPICS, description: 'Required-topic id, e.g. human-trafficking, implicit-bias, infection-control, opioids-pain, laws-rules.' }, 45 free_only: { type: 'boolean', description: 'Only completely free courses: no account and no certificate fee. Defaults to false.' }, 46 }, 47 required: [], 48 additionalProperties: false, 49 }, 50 deepLink: (a = {}) => deepLink('/free-nursing-ceu', { state: a.state, license: a.license, topic: a.topic }), 51 run(rawArgs) { 52 const a = rawArgs || {}; 53 const state = a.state ? String(a.state).toUpperCase() : ''; 54 const st = state ? CEU.states[state] : null; 55 let rows = CEU.courses; 56 if (a.license) rows = rows.filter((c) => (c.licenses || ['RN', 'LPN', 'APRN']).includes(String(a.license).toUpperCase())); 57 if (a.topic) rows = rows.filter((c) => c.topics.includes(String(a.topic).toLowerCase())); 58 if (a.free_only === true) rows = rows.filter((c) => c.cost === 'free'); 59 rows = rows.filter((c) => !(c.states && c.states.length) || (state && c.states.includes(state))); 60 const byId = Object.fromEntries(CEU.courses.map((c) => [c.id, c])); 61 const lines = []; 62 if (st) { 63 lines.push('', `${st.name}: ${st.total}.`); 64 if (!st.mandates.length) lines.push(` ${st.name} names no specific CE topic for RN renewal.`); 65 for (const m of st.mandates) { 66 const bits = [m.hours != null ? `${m.hours} h` : '', m.frequency, (m.applies_to || []).join('/')].filter(Boolean).join(', '); 67 const free = (m.free_course_ids || []).map((id) => byId[id]).filter(Boolean); 68 lines.push(` ${m.label}${bits ? ` (${bits})` : ''}${m.provider_rule ? ` â ${m.provider_rule}` : ''}`); 69 lines.push(free.length ? ` free: ${free.map((c) => `${c.title} (${c.hours} h, ${c.url})`).join('; ')}` 70 : ' no free course found yet'); 71 } 72 } 73 if (!rows.length && !st) { 74 return 'No free course matched those filters. Try dropping the topic or free_only. ' 75 + `Topics: ${CEU_TOPICS.join(', ')}.`; 76 } 77 const shown = rows.slice(0, LIMIT); 78 const free = rows.filter((c) => c.cost === 'free').length; 79 return formatToolResult({ 80 headline: `${rows.length} free course${rows.length === 1 ? '' : 's'} match, ${free} of them with no account and no certificate fee.`, 81 lines: lines.concat(shown.flatMap((c) => ['', 82 `${c.title} â ${c.provider}, ${c.hours} contact hours, ${COST_WORD[c.cost]}${c.certificate_fee ? ` (${c.certificate_fee})` : ''}`, 83 ` ${c.accreditor}; checked ${c.verified}`, 84 ` ${c.url}`, 85 ])).concat(rows.length > shown.length ? ['', `+ ${rows.length - shown.length} more on the page.`] : []), 86 provenance: 'each course checked on its own page; each mandate against the board, code or statute', 87 link: deepLink('/free-nursing-ceu', { state, license: a.license, topic: a.topic }), 88 nextStep: 'A topic a board requires from its own approved providers only counts from one of them.', 89 }); 90 }, 91}; 92 93const residencyTool = { 94 name: 'find_nurse_residency_program', 95 title: 'Nurse Residency Finder', 96 kind: 'lookup', 97 pages: ['nurse-residency-finder'], 98 description: 99 'Find new-graduate nurse residency programs, filtered by state, specialty and ' 100 + 'accreditation. Returns the health system, which states it runs in, programme length, ' 101 + 'specialties accepted, whether it is CCNE/ANCC PTAP accredited, whether the system is ' 102 + 'Magnet, typical pay and cohort size. Use this for new-grad RNs choosing a first job â ' 103 + 'an accredited residency is the single biggest predictor of first-year retention.', 104 annotations: { readOnlyHint: true }, 105 inputSchema: { 106 type: 'object', 107 properties: { 108 state: { type: 'string', enum: RES_STATES, description: 'Two-letter state code the programme must run in.' }, 109 specialty: { type: 'string', enum: RES_SPEC, description: 'Specialty track: ICU, ER, MS, Peds, OB, Psych, OR, Onc, NICU or PACU.' }, 110 accredited_only: { type: 'boolean', description: 'Return only CCNE/ANCC PTAP-accredited programmes. Defaults to false.' }, 111 }, 112 required: [], 113 additionalProperties: false, 114 }, 115 deepLink: (a = {}) => deepLink('/nurse-residency-finder', { state: a.state, spec: a.specialty }), 116 run(rawArgs) { 117 const a = rawArgs || {}; 118 let rows = RESIDENCY.programs; 119 if (a.state) rows = rows.filter((p) => p.states.includes(String(a.state).toUpperCase())); 120 if (a.specialty) { 121 const s = String(a.specialty).toLowerCase(); 122 rows = rows.filter((p) => p.spec.some((x) => x.toLowerCase() === s)); 123 } 124 if (a.accredited_only === true) rows = rows.filter((p) => p.accred); 125 // Accredited first, then Magnet â the two signals that actually predict the experience. 126 rows = [...rows].sort((x, y) => (y.accred - x.accred) || (y.magnet - x.magnet)); 127 if (!rows.length) { 128 return `No residency programme matched. This directory covers ${RESIDENCY.programs.length} ` 129 + `national and regional programmes, not every hospital â most systems run one even if ` 130 + `they are not listed, so check the careers page of the systems in your area too. ` 131 + `States covered: ${RES_STATES.join(', ')}.`; 132 } 133 const shown = rows.slice(0, LIMIT); 134 return formatToolResult({ 135 headline: `${rows.length} residency programme${rows.length === 1 ? '' : 's'} match` 136 + `${a.state ? ` in ${String(a.state).toUpperCase()}` : ''}, ` 137 + `${rows.filter((p) => p.accred).length} accredited.`, 138 lines: shown.flatMap((p) => ['', 139 `${p.n}
139${p.accred ? ' · ACCREDITED' : ''}${p.magnet ? ' · Magnet' : ''}`, 140 ` ${p.loc} · ${p.months} months · ${p.spec.join(', ')}`, 141 ` Pay: ${p.pay} · Cohort: ${p.size}`, 142 ` ${p.url}`, 143 ]).concat(rows.length > shown.length ? ['', `+ ${rows.length - shown.length} more on the page.`] : []), 144 provenance: 'programme details as published by each health system; accreditation is CCNE or ANCC PTAP', 145 link: deepLink('/nurse-residency-finder', { state: a.state, spec: a.specialty }), 146 nextStep: 'Application windows are narrow and mostly cohort-based â the page lists each ' 147 + "programme's window, and missing it usually means waiting six months.", 148 }); 149 }, 150}; 151 152const mentalHealthTool = { 153 name: 'find_nurse_mental_health_support', 154 title: 'Nurse Mental Health Resources', 155 kind: 'lookup', 156 pages: ['nurse-mental-health-finder'], 157 description: 158 'Find mental-health and crisis support for nurses: crisis lines, peer support, therapy ' 159 + 'matched to clinicians, and addiction/recovery programmes including confidential ' 160 + 'alternative-to-discipline options. Returns each resource with what it is, what it ' 161 + 'costs and how to reach it. Use this whenever a nurse mentions burnout, suicidal ' 162 + 'thoughts, substance use, or losing their licence over either â and lead with the ' 163 + 'crisis lines, which are free and answer immediately.', 164 annotations: { readOnlyHint: true }, 165 inputSchema: { 166 type: 'object', 167 properties: { 168 category: { type: 'string', enum: MH_CAT, description: 'Kind of support: "crisis" (immediate), "peer" (nurse-to-nurse), "therapy" (clinician-matched), or "group".' }, 169 free_only: { type: 'boolean', description: 'Return only free resources. Defaults to false.' }, 170 state: { type: 'string', description: 'Two-letter state code. Nationwide resources are always included.' }, 171 }, 172 required: [], 173 additionalProperties: false, 174 }, 175 deepLink: (a = {}) => deepLink('/nurse-mental-health-finder', { cat: a.category, state: a.state }), 176 run(rawArgs) { 177 const a = rawArgs || {}; 178 let rows = MENTAL.resources; 179 if (a.category) rows = rows.filter((r) => r.cat === String(a.category).toLowerCase()); 180 if (a.free_only === true) rows = rows.filter((r) => r.cost === 'free'); 181 // Crisis first, always, whatever was asked for â then free, then the rest. 182 rows = [...rows].sort((x, y) => 183 (y.cat === 'crisis') - (x.cat === 'crisis') || (y.cost === 'free') - (x.cost === 'free')); 184 if (!rows.length) { 185 return 'No resource matched those filters, which should not happen for a nurse who needs ' 186 + 'one. Call or text 988 (Suicide & Crisis Lifeline, 24/7, free) and see ' 187 + `${deepLink('/nurse-mental-health-finder')} for the full list.`; 188 } 189 const shown = rows.slice(0, LIMIT); 190 return formatToolResult({ 191 headline: `${rows.length} resource${rows.length === 1 ? '' : 's'} available` 192 + `${rows.filter((r) => r.cost === 'free').length ? `, ${rows.filter((r) => r.cost === 'free').length} free` : ''}.`, 193 lines: shown.flatMap((r) => ['', 194 `${r.n} â ${r.cost}${r.nat ? ' · nationwide' : ''}`, 195 ` ${r.desc}`, 196 ` ${r.url}`, 197 ]), 198 caveat: 'If this is an emergency, 988 (call or text) is free, 24/7 and answers now. Nurses ' 199 + 'worried about their licence should know most states run a confidential ' 200 + 'alternative-to-discipline programme â using it is not the same as being reported.', 201 provenance: "verified against each organisation's own site", 202 link: deepLink('/nurse-mental-health-finder', { cat: a.category, state: a.state }), 203 nextStep: 'The page filters these by state and by whether your employer already covers them.', 204 }); 205 }, 206}; 207 208export default [ceuTool, residencyTool, mentalHealthTool];
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.