CareJobs.psm1
|
# CareJobs: PowerShell client for the CareJobs.Now healthcare jobs API. # # The API is a remote MCP server at https://carejobs.now/api/mcp that needs no key. # Every function sends one JSON-RPC request to it with Invoke-WebRequest and returns # the tool's data as objects. Data from CareJobs.Now (https://carejobs.now), CC BY 4.0. Set-StrictMode -Version 3.0 $script:ModuleVersion = '1.0.0' $script:DefaultBaseUrl = 'https://carejobs.now' $script:BaseUrl = $script:DefaultBaseUrl $script:TimeoutSec = 60 $script:UserAgent = "carejobs-powershell/$script:ModuleVersion" $script:NextId = 0 $script:Kinds = @( 'nurses', 'nurse-practitioners', 'physicians', 'nursing-assistants', 'caregivers', 'therapy-rehab', 'mental-health', 'pharmacy', 'imaging', 'lab', 'surgical-techs', 'emts-paramedics', 'medical-assistants', 'dental', 'admin', 'allied-health', 'ai-training' ) # Windows PowerShell 5.1 on older .NET Framework may not offer TLS 1.2 by default. if ($PSVersionTable.PSEdition -ne 'Core') { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } # -- private helpers ------------------------------------------------------------------ function New-CareJobsErrorRecord { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds an object in memory and changes no state.')] [OutputType([System.Management.Automation.ErrorRecord])] param( [string] $Message, [string] $Reason, [string] $Tool, [object] $Status, [object] $RpcCode, [Exception] $Inner ) $exception = New-Object System.Exception -ArgumentList $Message, $Inner $exception.Data['Reason'] = $Reason $exception.Data['Tool'] = $Tool $exception.Data['Status'] = $Status $exception.Data['RpcCode'] = $RpcCode $category = switch ($Reason) { 'tool' { [System.Management.Automation.ErrorCategory]::InvalidArgument } 'rpc' { [System.Management.Automation.ErrorCategory]::InvalidOperation } 'http' { [System.Management.Automation.ErrorCategory]::InvalidResult } 'network' { [System.Management.Automation.ErrorCategory]::ConnectionError } 'timeout' { [System.Management.Automation.ErrorCategory]::OperationTimeout } default { [System.Management.Automation.ErrorCategory]::ProtocolError } } New-Object System.Management.Automation.ErrorRecord -ArgumentList $exception, "CareJobs.$Reason", $category, $Tool } function ConvertFrom-CareJobsJson { param([string] $Json, [string] $Tool) try { $value = $Json | ConvertFrom-Json -ErrorAction Stop } catch { throw (New-CareJobsErrorRecord -Message 'The server did not answer with JSON' -Reason 'protocol' -Tool $Tool -Inner $_.Exception) } if ($null -eq $value -or $value -isnot [System.Management.Automation.PSCustomObject]) { throw (New-CareJobsErrorRecord -Message 'The server did not answer with a JSON-RPC object' -Reason 'protocol' -Tool $Tool) } $value } function Test-CareJobsProperty { param($Object, [string] $Name) $null -ne $Object -and $Object -is [System.Management.Automation.PSCustomObject] -and $null -ne $Object.PSObject.Properties[$Name] } # Reads the JSON-RPC message out of a plain JSON body or a text/event-stream body. function Read-CareJobsMessage { param([string] $Body, [string] $ContentType, [int] $Id, [string] $Tool) if ($ContentType -notmatch 'text/event-stream') { return (ConvertFrom-CareJobsJson -Json $Body -Tool $Tool) } $fallback = $null foreach ($eventText in ($Body -replace "`r`n", "`n") -split "`n`n") { $data = @() foreach ($line in $eventText -split "`n") { if ($line.StartsWith('data:')) { $data += $line.Substring(5).TrimStart(' ') } } if ($data.Count -eq 0) { continue } try { $message = ConvertFrom-CareJobsJson -Json ($data -join "`n") -Tool $Tool } catch { continue } if ((Test-CareJobsProperty $message 'id') -and $message.id -is [ValueType] -and $message.id -eq $Id) { return $message } if ($null -eq $fallback -and ((Test-CareJobsProperty $message 'result') -or (Test-CareJobsProperty $message 'error'))) { $fallback = $message } } if ($null -ne $fallback) { return $fallback } throw (New-CareJobsErrorRecord -Message 'The event stream held no JSON-RPC answer' -Reason 'protocol' -Tool $Tool) } function Get-CareJobsHttpErrorDetail { param([int] $Status, [string] $Body, [string] $ContentType, [int] $Id) $detail = "HTTP $Status" try { $message = Read-CareJobsMessage -Body $Body -ContentType $ContentType -Id $Id -Tool $null if ((Test-CareJobsProperty $message 'error') -and (Test-CareJobsProperty $message.error 'message') -and $message.error.message) { return "${detail}: $($message.error.message)" } } catch { Write-Debug "The error body is not JSON-RPC, so its raw text is used: $_" } $text = "$Body".Trim() if ($text.Length -eq 0) { return $detail } if ($text.Length -gt 200) { $text = $text.Substring(0, 200) } "${detail}: $text" } function Get-CareJobsHeaderValue { param($Headers, [string] $Name) if ($null -eq $Headers) { return '' } foreach ($key in @($Headers.Keys)) { if ($key -eq $Name) { return [string](@($Headers[$key])[0]) } } '' } # Sends one JSON-RPC request and returns the result of its answer. function Invoke-CareJobsRpc { param( [Parameter(Mandatory)] [string] $Method, [Parameter(Mandatory)] [System.Collections.IDictionary] $Params, [string] $Tool ) $script:NextId++ $id = $script:NextId $payload = [ordered]@{ jsonrpc = '2.0'; id = $id; method = $Method; params = $Params } $json = $payload | ConvertTo-Json -Depth 20 -Compress $uri = $script:BaseUrl.TrimEnd('/') + '/api/mcp' $request = @{ Uri = $uri Method = 'Post' Body = [System.Text.Encoding]::UTF8.GetBytes($json) ContentType = 'application/json' Headers = @{ Accept = 'application/json, text/event-stream' } UserAgent = $script:UserAgent TimeoutSec = $script:TimeoutSec UseBasicParsing = $true ErrorAction = 'Stop' } if ($PSVersionTable.PSVersion.Major -ge 7) { $request['SkipHttpErrorCheck'] = $true } $status = 0 $contentType = '' $body = '' try { $response = Invoke-WebRequest @request $status = [int]$response.StatusCode $contentType = Get-CareJobsHeaderValue $response.Headers 'Content-Type' $body = [System.Text.Encoding]::UTF8.GetString($response.RawContentStream.ToArray()) } catch [System.Net.WebException] { # Windows PowerShell 5.1: a non-2xx status, or no answer at all. $webError = $_.Exception if ($null -ne $webError.Response) { $status = [int]$webError.Response.StatusCode $contentType = [string]$webError.Response.ContentType $reader = New-Object System.IO.StreamReader -ArgumentList $webError.Response.GetResponseStream(), ([System.Text.Encoding]::UTF8) try { $body = $reader.ReadToEnd() } finally { $reader.Dispose() } } elseif ($webError.Status -eq [System.Net.WebExceptionStatus]::Timeout) { throw (New-CareJobsErrorRecord -Message "No answer from $uri within $($script:TimeoutSec) s" -Reason 'timeout' -Tool $Tool -Inner $webError) } else { throw (New-CareJobsErrorRecord -Message "Could not reach ${uri}: $($webError.Message)" -Reason 'network' -Tool $Tool -Inner $webError) } } catch { $failure = $_.Exception $isTimeout = $false for ($e = $failure; $null -ne $e; $e = $e.InnerException) { if ($e -is [System.TimeoutException] -or $e -is [System.Threading.Tasks.TaskCanceledException] -or $e.Message -match 'Timeout|timed out') { $isTimeout = $true } } if ($isTimeout) { throw (New-CareJobsErrorRecord -Message "No answer from $uri within $($script:TimeoutSec) s" -Reason 'timeout' -Tool $Tool -Inner $failure) } throw (New-CareJobsErrorRecord -Message "Could not reach ${uri}: $($failure.Message)" -Reason 'network' -Tool $Tool -Inner $failure) } if ($status -lt 200 -or $status -gt 299) { $detail = Get-CareJobsHttpErrorDetail -Status $status -Body $body -ContentType $contentType -Id $id throw (New-CareJobsErrorRecord -Message $detail -Reason 'http' -Tool $Tool -Status $status) } $message = Read-CareJobsMessage -Body $body -ContentType $contentType -Id $id -Tool $Tool if ((Test-CareJobsProperty $message 'error') -and $null -ne $message.error) { $rpcError = $message.error if ($rpcError -isnot [System.Management.Automation.PSCustomObject]) { throw (New-CareJobsErrorRecord -Message 'Malformed JSON-RPC error' -Reason 'protocol' -Tool $Tool) } $text = 'JSON-RPC error' if ((Test-CareJobsProperty $rpcError 'message') -and $rpcError.message) { $text = [string]$rpcError.message } $code = $null if (Test-CareJobsProperty $rpcError 'code') { $code = $rpcError.code } throw (New-CareJobsErrorRecord -Message $text -Reason 'rpc' -Tool $Tool -RpcCode $code) } if (-not (Test-CareJobsProperty $message 'result') -or $message.result -isnot [System.Management.Automation.PSCustomObject]) { throw (New-CareJobsErrorRecord -Message 'The answer held no result' -Reason 'protocol' -Tool $Tool) } $message.result } # Calls one tool and returns @{ Data = ...; Text = ... }. function Invoke-CareJobsToolCall { param( [Parameter(Mandatory)] [string] $Name, [System.Collections.IDictionary] $Arguments ) $clean = [ordered]@{} if ($null -ne $Arguments) { foreach ($key in @($Arguments.Keys)) { if ($null -ne $Arguments[$key]) { $clean[[string]$key] = $Arguments[$key] } } } $raw = Invoke-CareJobsRpc -Method 'tools/call' -Params ([ordered]@{ name = $Name; arguments = $clean }) -Tool $Name $texts = @() if ((Test-CareJobsProperty $raw 'content') -and $null -ne $raw.content) { foreach ($part in @($raw.content)) { if ((Test-CareJobsProperty $part 'type') -and $part.type -eq 'text' -and (Test-CareJobsProperty $part 'text') -and $part.text -is [string]) { $texts += $part.text } } } $text = $texts -join "`n" if ((Test-CareJobsProperty $raw 'isError') -and $raw.isError -eq $true) { if (-not $text) { $text = "$Name failed" } throw (New-CareJobsErrorRecord -Message $text -Reason 'tool' -Tool $Name) } if (-not (Test-CareJobsProperty $raw 'structuredContent') -or $raw.structuredContent -isnot [System.Management.Automation.PSCustomObject]) { throw (New-CareJobsErrorRecord -Message "$Name answered without structured content" -Reason 'protocol' -Tool $Name) } [pscustomobject]@{ Data = $raw.structuredContent; Text = $text } } function Write-CareJobsAnswer { param($Answer, [switch] $Summary) if ($Summary) { $Answer.Text } else { $Answer.Data } } # -- public functions ----------------------------------------------------------------- function Search-CareJobs { <# .SYNOPSIS Searches the open US healthcare jobs on CareJobs.Now. .DESCRIPTION Every filter is optional. By default each job is written to the pipeline, with title, employer, kind, pay, schedule, location, listed date and url (the page on CareJobs.Now where the job is applied for). -Raw returns the whole answer instead (total, pay, jobs and more), and -Summary the server's readable text. .PARAMETER Query Words every job must carry in its title, place, department or schedule ("ICU", "night", "Houston"). .PARAMETER Kind A kind of job, for example nurses or nursing-assistants. Get-CareJobsKind lists them. .PARAMETER Employer An employer slug ("kaiser-permanente") or name. .PARAMETER State A US state: TX or Texas. .PARAMETER Remote Keeps only remote jobs. .PARAMETER MinHourlyUsd Only jobs whose posted hourly ceiling reaches this (1 to 1000). .PARAMETER Sort mixed (the default), newest or highest_pay. .PARAMETER Limit How many jobs to return, 1 to 25 (default 10). .PARAMETER Raw Returns the whole answer object (total, pay, jobs, more) instead of the jobs. .PARAMETER Summary Returns the server's readable summary as text. .EXAMPLE Search-CareJobs -Query ICU -Kind nurses -State TX -Limit 5 | Format-Table title, employer, pay, location .EXAMPLE (Search-CareJobs -Kind caregivers -State OH -Raw).total .EXAMPLE Search-CareJobs night -Kind nurses -Sort highest_pay -Summary .LINK https://carejobs.now/data #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'CareJobs is the name of the service, not a plural.')] [CmdletBinding()] [OutputType([pscustomobject], [string])] param( [Parameter(Position = 0)] [string] $Query, [string] $Kind, [string] $Employer, [string] $State, [switch] $Remote, [ValidateRange(1, 1000)] [int] $MinHourlyUsd, [ValidateSet('mixed', 'newest', 'highest_pay')] [string] $Sort, [ValidateRange(1, 25)] [int] $Limit, [switch] $Raw, [switch] $Summary ) $arguments = [ordered]@{} if ($Query) { $arguments.query = $Query } if ($Kind) { $arguments.kind = $Kind } if ($Employer) { $arguments.employer = $Employer } if ($State) { $arguments.state = $State } if ($Remote) { $arguments.remote = $true } if ($PSBoundParameters.ContainsKey('MinHourlyUsd')) { $arguments.min_hourly_usd = $MinHourlyUsd } if ($Sort) { $arguments.sort = $Sort } if ($PSBoundParameters.ContainsKey('Limit')) { $arguments.limit = $Limit } try { $answer = Invoke-CareJobsToolCall -Name 'search_healthcare_jobs' -Arguments $arguments } catch { $PSCmdlet.ThrowTerminatingError($_) } if ($Summary) { return $answer.Text } if ($Raw) { return $answer.Data } if (Test-CareJobsProperty $answer.Data 'jobs') { foreach ($job in @($answer.Data.jobs)) { if ($null -ne $job) { $job.PSObject.TypeNames.Insert(0, 'CareJobs.Job') $job } } } } function Get-CareJobsPay { <# .SYNOPSIS Gets the hourly pay US healthcare jobs post, measured from open jobs on CareJobs.Now. .DESCRIPTION Pay is what each posting states, before shift differentials and bonuses. With no parameters: every kind of job. With -Kind: that job's pay and the employers posting the most for it. With -Employer: its pay overall and by kind. With both: that employer's pay for that kind. .PARAMETER Kind A kind of job, for example nurses. Get-CareJobsKind lists them. .PARAMETER Employer An employer slug ("kaiser-permanente") or name. .PARAMETER Summary Returns the server's readable summary as text. .EXAMPLE (Get-CareJobsPay -Kind nurses).pay.median .EXAMPLE Get-CareJobsPay -Employer kaiser-permanente -Summary .LINK https://carejobs.now/data #> [CmdletBinding()] [OutputType([pscustomobject], [string])] param( [Parameter(Position = 0)] [string] $Kind, [string] $Employer, [switch] $Summary ) $arguments = [ordered]@{} if ($Kind) { $arguments.kind = $Kind } if ($Employer) { $arguments.employer = $Employer } try { $answer = Invoke-CareJobsToolCall -Name 'get_healthcare_pay' -Arguments $arguments } catch { $PSCmdlet.ThrowTerminatingError($_) } Write-CareJobsAnswer $answer -Summary:$Summary } function Get-CareJobsMarket { <# .SYNOPSIS Gets how many US healthcare jobs are open on CareJobs.Now, and where. .DESCRIPTION How many jobs are open, how many were first listed in the last 7 days, and the open jobs by kind of job, by state and by employer. .PARAMETER Summary Returns the server's readable summary as text. .EXAMPLE Get-CareJobsMarket -Summary .LINK https://carejobs.now/data #> [CmdletBinding()] [OutputType([pscustomobject], [string])] param([switch] $Summary) try { $answer = Invoke-CareJobsToolCall -Name 'get_healthcare_job_market' -Arguments @{} } catch { $PSCmdlet.ThrowTerminatingError($_) } Write-CareJobsAnswer $answer -Summary:$Summary } function Get-CareJobsByState { <# .SYNOPSIS Gets the open US healthcare jobs by state. .DESCRIPTION With no state: every state. With -State: its jobs by kind, pay, towns, employers and newest jobs. With -State and -Kind: the same for that kind (a kind needs a state). .PARAMETER State A US state: OH or Ohio. .PARAMETER Kind A kind of job, for example nursing-assistants. Needs -State. .PARAMETER Limit How many of the newest jobs to return, 1 to 25. .PARAMETER Summary Returns the server's readable summary as text. .EXAMPLE (Get-CareJobsByState OH -Kind nursing-assistants).open_jobs .EXAMPLE Get-CareJobsByState -Summary .LINK https://carejobs.now/data #> [CmdletBinding()] [OutputType([pscustomobject], [string])] param( [Parameter(Position = 0)] [string] $State, [string] $Kind, [ValidateRange(1, 25)] [int] $Limit, [switch] $Summary ) $arguments = [ordered]@{} if ($State) { $arguments.state = $State } if ($Kind) { $arguments.kind = $Kind } if ($PSBoundParameters.ContainsKey('Limit')) { $arguments.limit = $Limit } try { $answer = Invoke-CareJobsToolCall -Name 'get_healthcare_jobs_by_state' -Arguments $arguments } catch { $PSCmdlet.ThrowTerminatingError($_) } Write-CareJobsAnswer $answer -Summary:$Summary } function Get-CareJobsEmployer { <# .SYNOPSIS Lists the healthcare employers CareJobs.Now reads, or gets one of them. .DESCRIPTION Without -Slug: the employers (and AI-training programs and job boards) the site reads, most open jobs first, each written to the pipeline (-Raw returns the whole answer). With -Slug: one employer, AI-training program or job board, by slug or name: what it is, where it hires, its open jobs by kind, the pay it posts, its newest and best-paid jobs, and links to its pages. A slug can come from the pipeline, so a list can be piped into this function. .PARAMETER Slug One employer: its slug ("kaiser-permanente") or name. .PARAMETER Query Part of the name ("Kaiser", "children"). .PARAMETER Type employer, ai-program or job-board. .PARAMETER Care A kind of care, for example home-care or health-system. .PARAMETER State Keeps employers with open jobs in this state. .PARAMETER Kind Keeps employers with open jobs of this kind. .PARAMETER Limit How many employers to return, 1 to 100 (default 20). .PARAMETER Raw Returns the whole list answer instead of the entries. .PARAMETER Summary Returns the server's readable summary as text. .EXAMPLE Get-CareJobsEmployer -Query Kaiser -Limit 3 | Format-Table name, open_jobs .EXAMPLE Get-CareJobsEmployer -Slug kaiser-permanente -Summary .LINK https://carejobs.now/data #> [CmdletBinding(DefaultParameterSetName = 'List')] [OutputType([pscustomobject], [string])] param( [Parameter(Mandatory, ParameterSetName = 'One', ValueFromPipelineByPropertyName)] [string] $Slug, [Parameter(Position = 0, ParameterSetName = 'List')] [string] $Query, [Parameter(ParameterSetName = 'List')] [ValidateSet('employer', 'ai-program', 'job-board')] [string] $Type, [Parameter(ParameterSetName = 'List')] [string] $Care, [Parameter(ParameterSetName = 'List')] [string] $State, [Parameter(ParameterSetName = 'List')] [string] $Kind, [Parameter(ParameterSetName = 'List')] [ValidateRange(1, 100)] [int] $Limit, [Parameter(ParameterSetName = 'List')] [switch] $Raw, [switch] $Summary ) process { try { if ($PSCmdlet.ParameterSetName -eq 'One') { $answer = Invoke-CareJobsToolCall -Name 'get_healthcare_employer' -Arguments ([ordered]@{ employer = $Slug }) } else { $arguments = [ordered]@{} if ($Query) { $arguments.query = $Query } if ($Type) { $arguments.type = $Type } if ($Care) { $arguments.care = $Care } if ($State) { $arguments.state = $State } if ($Kind) { $arguments.kind = $Kind } if ($PSBoundParameters.ContainsKey('Limit')) { $arguments.limit = $Limit } $answer = Invoke-CareJobsToolCall -Name 'list_healthcare_employers' -Arguments $arguments } } catch { $PSCmdlet.ThrowTerminatingError($_) } if ($Summary) { return $answer.Text } if ($PSCmdlet.ParameterSetName -eq 'One' -or $Raw) { return $answer.Data } if (Test-CareJobsProperty $answer.Data 'entries') { foreach ($entry in @($answer.Data.entries)) { if ($null -ne $entry) { $entry.PSObject.TypeNames.Insert(0, 'CareJobs.Employer') $entry } } } } } function Get-CareJobsKind { <# .SYNOPSIS Lists the kinds of healthcare job the CareJobs.Now API knows. .DESCRIPTION Returns the 17 kind slugs (nurses, nurse-practitioners, physicians, ...) that the -Kind parameters take. Works offline. .EXAMPLE Get-CareJobsKind #> [CmdletBinding()] [OutputType([string])] param() foreach ($kind in $script:Kinds) { $kind } } function Get-CareJobsTool { <# .SYNOPSIS Lists the tools the CareJobs.Now MCP server offers, with their input schemas. .EXAMPLE Get-CareJobsTool | Select-Object name, description .LINK https://carejobs.now/api/mcp #> [CmdletBinding()] [OutputType([pscustomobject])] param() try { $result = Invoke-CareJobsRpc -Method 'tools/list' -Params ([ordered]@{}) if (-not (Test-CareJobsProperty $result 'tools') -or $null -eq $result.tools) { throw (New-CareJobsErrorRecord -Message 'tools/list answered without a tools list' -Reason 'protocol') } } catch { $PSCmdlet.ThrowTerminatingError($_) } foreach ($tool in @($result.tools)) { $tool } } function Invoke-CareJobsTool { <# .SYNOPSIS Calls any tool of the CareJobs.Now MCP server by name. .DESCRIPTION For a tool added after this version of the module. Arguments keep the API's names; $null values are left out. Returns the tool's data, or with -Summary its readable text. .PARAMETER Name The tool's name, for example search_healthcare_jobs. Get-CareJobsTool lists them. .PARAMETER Arguments The tool's arguments as a hashtable. .PARAMETER Summary Returns the server's readable summary as text. .EXAMPLE Invoke-CareJobsTool -Name search_healthcare_jobs -Arguments @{ kind = 'lab'; state = 'NY'; limit = 3 } .LINK https://carejobs.now/api/mcp #> [CmdletBinding()] [OutputType([pscustomobject], [string])] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string] $Name, [Parameter(Position = 1)] [System.Collections.IDictionary] $Arguments = @{}, [switch] $Summary ) try { $answer = Invoke-CareJobsToolCall -Name $Name -Arguments $Arguments } catch { $PSCmdlet.ThrowTerminatingError($_) } Write-CareJobsAnswer $answer -Summary:$Summary } function Set-CareJobsOption { <# .SYNOPSIS Changes the host, timeout or User-Agent the CareJobs functions use in this session. .DESCRIPTION The defaults are https://carejobs.now, 60 seconds and carejobs-powershell/<version>. The server answers a query it has not seen before in up to about 25 seconds, then repeats of it in a fraction of one, which is why the default timeout is a minute. .PARAMETER BaseUrl The host; the MCP endpoint is <BaseUrl>/api/mcp. .PARAMETER TimeoutSec Seconds one call may take. .PARAMETER UserAgent Sent as the User-Agent header. .EXAMPLE Set-CareJobsOption -TimeoutSec 120 -UserAgent 'my-script/1.0' #> [CmdletBinding(SupportsShouldProcess)] param( [ValidatePattern('^https?://[^/\s]+')] [string] $BaseUrl, [ValidateRange(1, 3600)] [int] $TimeoutSec, [ValidateNotNullOrEmpty()] [string] $UserAgent ) if ($PSCmdlet.ShouldProcess('CareJobs module options', 'Set')) { if ($BaseUrl) { $script:BaseUrl = $BaseUrl.TrimEnd('/') } if ($PSBoundParameters.ContainsKey('TimeoutSec')) { $script:TimeoutSec = $TimeoutSec } if ($UserAgent) { $script:UserAgent = $UserAgent } } } # Tab completion of the kind slugs; any other string is still accepted, so a kind # the server adds later works without a new version of this module. Register-ArgumentCompleter -CommandName Search-CareJobs, Get-CareJobsPay, Get-CareJobsByState, Get-CareJobsEmployer -ParameterName Kind -ScriptBlock { param($commandName, $parameterName, $wordToComplete) $null = $commandName, $parameterName $script:Kinds | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { New-Object System.Management.Automation.CompletionResult -ArgumentList $_, $_, 'ParameterValue', $_ } } Export-ModuleMember -Function Search-CareJobs, Get-CareJobsPay, Get-CareJobsMarket, Get-CareJobsByState, Get-CareJobsEmployer, Get-CareJobsKind, Get-CareJobsTool, Invoke-CareJobsTool, Set-CareJobsOption |