PageSourceSearch

https://thenursingdirectory.com/agent-tools/tools/find-nursing-resources.js?v=5323ecb261

js thenursingdirectory.com collected 2026-10-07 19:36:04 UTC 11,733 bytes, 208 lines download raw bytes

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.