diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..36bd853 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [StartAutomating] diff --git a/.github/workflows/BuildOllamaPowerShell.yml b/.github/workflows/BuildOllamaPowerShell.yml new file mode 100644 index 0000000..7966227 --- /dev/null +++ b/.github/workflows/BuildOllamaPowerShell.yml @@ -0,0 +1,509 @@ + +name: Build ollama-powershell +on: + push: + pull_request: + workflow_dispatch: +jobs: + TestPowerShellOnLinux: + runs-on: ubuntu-latest + steps: + - name: InstallPester + id: InstallPester + shell: pwsh + run: | + $Parameters = @{} + $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + Installs Pester + .Description + Installs Pester + #> + param( + # The maximum pester version. Defaults to 4.99.99. + [string] + $PesterMaxVersion = '4.99.99' + ) + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber + Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters + - name: Check out repository + uses: actions/checkout@v4 + - name: RunPester + id: RunPester + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} + $Parameters.NoCoverage = ${env:NoCoverage} + $Parameters.NoCoverage = $parameters.NoCoverage -match 'true'; + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + Runs Pester + .Description + Runs Pester tests after importing a PowerShell module + #> + param( + # The module path. If not provided, will default to the second half of the repository ID. + [string] + $ModulePath, + # The Pester max version. By default, this is pinned to 4.99.99. + [string] + $PesterMaxVersion = '4.99.99', + + # If set, will not collect code coverage. + [switch] + $NoCoverage + ) + + $global:ErrorActionPreference = 'continue' + $global:ProgressPreference = 'silentlycontinue' + + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" } + $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion + $importedModule = Import-Module $ModulePath -Force -PassThru + $importedPester, $importedModule | Out-Host + + $codeCoverageParameters = @{ + CodeCoverage = "$($importedModule | Split-Path)\*-*.ps1" + CodeCoverageOutputFile = ".\$moduleName.Coverage.xml" + } + + if ($NoCoverage) { + $codeCoverageParameters = @{} + } + + + $result = + Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml @codeCoverageParameters + + if ($result.FailedCount -gt 0) { + "::debug:: $($result.FailedCount) tests failed" + foreach ($r in $result.TestResult) { + if (-not $r.Passed) { + "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)" + } + } + throw "::error:: $($result.FailedCount) tests failed" + } + } @Parameters + - name: PublishTestResults + uses: actions/upload-artifact@main + with: + name: PesterResults + path: '**.TestResults.xml' + if: ${{always()}} + TagReleaseAndPublish: + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@v2 + - name: TagModuleVersion + id: TagModuleVersion + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.TagVersionFormat = ${env:TagVersionFormat} + $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # The tag version format (default value: 'v$(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagVersionFormat = 'v$($imported.Version)', + + # The tag version format (default value: '$($imported.Name) $(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagAnnotationFormat = '$($imported.Name) $($imported.Version)' + ) + + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping Tagging" | Out-Host + return + } + + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) + $existingTags = git tag --list + + @" + Target Version: $targetVersion + + Existing Tags: + $($existingTags -join [Environment]::NewLine) + "@ | Out-Host + + $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion } + + if ($versionTagExists) { + "::warning::Version $($versionTagExists)" + return + } + + if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } + if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } + git config --global user.email $UserEmail + git config --global user.name $UserName + + git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat) + git push origin --tags + + if ($env:GITHUB_ACTOR) { + exit 0 + }} @Parameters + - name: ReleaseModule + id: ReleaseModule + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.TagVersionFormat = ${env:TagVersionFormat} + $Parameters.ReleaseNameFormat = ${env:ReleaseNameFormat} + $Parameters.ReleaseAsset = ${env:ReleaseAsset} + $Parameters.ReleaseAsset = $parameters.ReleaseAsset -split ';' -replace '^[''"]' -replace '[''"]$' + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # The tag version format (default value: 'v$(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagVersionFormat = 'v$($imported.Version)', + + # The release name format (default value: '$($imported.Name) $($imported.Version)') + [string] + $ReleaseNameFormat = '$($imported.Name) $($imported.Version)', + + # Any assets to attach to the release. Can be a wildcard or file name. + [string[]] + $ReleaseAsset + ) + + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping GitHub release" | Out-Host + return + } + + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) + $targetReleaseName = $targetVersion + $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases' + "Release URL: $releasesURL" | Out-Host + $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } + + $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion + + if ($releaseExists) { + "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host + $releasedIt = $releaseExists + } else { + $releasedIt = Invoke-RestMethod -Uri $releasesURL -Method Post -Body ( + [Ordered]@{ + owner = '${{github.owner}}' + repo = '${{github.repository}}' + tag_name = $targetVersion + name = $ExecutionContext.InvokeCommand.ExpandString($ReleaseNameFormat) + body = + if ($env:RELEASENOTES) { + $env:RELEASENOTES + } elseif ($imported.PrivateData.PSData.ReleaseNotes) { + $imported.PrivateData.PSData.ReleaseNotes + } else { + "$($imported.Name) $targetVersion" + } + draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false } + prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false } + } | ConvertTo-Json + ) -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Content-type" = "application/json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } + } + + + + + + if (-not $releasedIt) { + throw "Release failed" + } else { + $releasedIt | Out-Host + } + + $releaseUploadUrl = $releasedIt.upload_url -replace '\{.+$' + + if ($ReleaseAsset) { + $fileList = Get-ChildItem -Recurse + $filesToRelease = + @(:nextFile foreach ($file in $fileList) { + foreach ($relAsset in $ReleaseAsset) { + if ($relAsset -match '[\*\?]') { + if ($file.Name -like $relAsset) { + $file; continue nextFile + } + } elseif ($file.Name -eq $relAsset -or $file.FullName -eq $relAsset) { + $file; continue nextFile + } + } + }) + + $releasedFiles = @{} + foreach ($file in $filesToRelease) { + if ($releasedFiles[$file.Name]) { + Write-Warning "Already attached file $($file.Name)" + continue + } else { + $fileBytes = [IO.File]::ReadAllBytes($file.FullName) + $releasedFiles[$file.Name] = + Invoke-RestMethod -Uri "${releaseUploadUrl}?name=$($file.Name)" -Headers @{ + "Accept" = "application/vnd.github+json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } -Body $fileBytes -ContentType Application/octet-stream + $releasedFiles[$file.Name] + } + } + + "Attached $($releasedFiles.Count) file(s) to release" | Out-Host + } + + + + } @Parameters + - name: PublishPowerShellGallery + id: PublishPowerShellGallery + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.Exclude = ${env:Exclude} + $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$' + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + [string[]] + $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif', 'docs[/\]*') + ) + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + if (-not $Exclude) { + $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif','docs[/\]*') + } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + @" + ::group::PSBoundParameters + $($PSBoundParameters | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping Gallery Publish" | Out-Host + return + } + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue} catch {} + + if ($foundModule -and (([Version]$foundModule.Version) -ge ([Version]$imported.Version))) { + "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host + } else { + + $gk = '${{secrets.GALLERYKEY}}' + + $rn = Get-Random + $moduleTempFolder = Join-Path $pwd "$rn" + $moduleTempPath = Join-Path $moduleTempFolder $moduleName + New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host + + Write-Host "Staging Directory: $ModuleTempPath" + + $imported | Split-Path | + Get-ChildItem -Force | + Where-Object Name -NE $rn | + Copy-Item -Destination $moduleTempPath -Recurse + + $moduleGitPath = Join-Path $moduleTempPath '.git' + Write-Host "Removing .git directory" + if (Test-Path $moduleGitPath) { + Remove-Item -Recurse -Force $moduleGitPath + } + + if ($Exclude) { + "::notice::Attempting to Exlcude $exclude" | Out-Host + Get-ChildItem $moduleTempPath -Recurse | + Where-Object { + foreach ($ex in $exclude) { + if ($_.FullName -like $ex) { + "::notice::Excluding $($_.FullName)" | Out-Host + return $true + } + } + } | + Remove-Item + } + + Write-Host "Module Files:" + Get-ChildItem $moduleTempPath -Recurse + Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery" + Publish-Module -Path $moduleTempPath -NuGetApiKey $gk + if ($?) { + Write-Host "Published to Gallery" + } else { + Write-Host "Gallery Publish Failed" + exit 1 + } + } + } @Parameters + BuildOllamaPowerShell: + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@v2 + - name: GitLogger + uses: GitLogging/GitLoggerAction@main + id: GitLogger + - name: Use PSSVG Action + uses: StartAutomating/PSSVG@main + id: PSSVG + - name: Use PipeScript Action + uses: StartAutomating/PipeScript@main + id: PipeScript + - name: UseEZOut + uses: StartAutomating/EZOut@master + - name: UseHelpOut + uses: StartAutomating/HelpOut@master +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} diff --git a/Assets/ollama-powershell-animated-icon.svg b/Assets/ollama-powershell-animated-icon.svg new file mode 100644 index 0000000..5a0d85a --- /dev/null +++ b/Assets/ollama-powershell-animated-icon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets/ollama-powershell-animated.svg b/Assets/ollama-powershell-animated.svg new file mode 100644 index 0000000..4e19f8e --- /dev/null +++ b/Assets/ollama-powershell-animated.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + ollama-powershell + diff --git a/Assets/ollama-powershell-icon.svg b/Assets/ollama-powershell-icon.svg new file mode 100644 index 0000000..9aa0268 --- /dev/null +++ b/Assets/ollama-powershell-icon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Assets/ollama-powershell.svg b/Assets/ollama-powershell.svg new file mode 100644 index 0000000..3f65d81 --- /dev/null +++ b/Assets/ollama-powershell.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + ollama-powershell + diff --git a/Build/GitHub/Jobs/BuildOllamaPowerShell.psd1 b/Build/GitHub/Jobs/BuildOllamaPowerShell.psd1 new file mode 100644 index 0000000..5be21b3 --- /dev/null +++ b/Build/GitHub/Jobs/BuildOllamaPowerShell.psd1 @@ -0,0 +1,40 @@ +@{ + "runs-on" = "ubuntu-latest" + if = '${{ success() }}' + steps = @( + @{ + name = 'Check out repository' + uses = 'actions/checkout@v2' + } + @{ + name = 'GitLogger' + uses = 'GitLogging/GitLoggerAction@main' + id = 'GitLogger' + } + @{ + name = 'Use PSSVG Action' + uses = 'StartAutomating/PSSVG@main' + id = 'PSSVG' + } + @{ + name = 'Use PipeScript Action' + uses = 'StartAutomating/PipeScript@main' + id = 'PipeScript' + } + 'RunEZOut' + 'RunHelpOut' + <#, + @{ + name = 'Use PSJekyll Action' + uses = 'PowerShellWeb/PSJekyll@main' + id = 'PSJekyll' + }#> + <#@{ + name = 'Run WebSocket (on branch)' + if = '${{github.ref_name != ''main''}}' + uses = './' + id = 'WebSocketAction' + },#> + # 'BuildAndPublishContainer' + ) +} \ No newline at end of file diff --git a/Build/GitHub/Steps/PublishTestResults.psd1 b/Build/GitHub/Steps/PublishTestResults.psd1 new file mode 100644 index 0000000..e8111e8 --- /dev/null +++ b/Build/GitHub/Steps/PublishTestResults.psd1 @@ -0,0 +1,10 @@ +@{ + name = 'PublishTestResults' + uses = 'actions/upload-artifact@main' + with = @{ + name = 'PesterResults' + path = '**.TestResults.xml' + } + if = '${{always()}}' +} + diff --git a/Build/ollama-powerShell.PSSVG.ps1 b/Build/ollama-powerShell.PSSVG.ps1 new file mode 100644 index 0000000..91cbe1e --- /dev/null +++ b/Build/ollama-powerShell.PSSVG.ps1 @@ -0,0 +1,115 @@ +#requires -Module PSSVG + +$assetsPath = Join-Path ($PSScriptRoot | Split-Path) Assets +if (-not (Test-Path $assetsPath)) { + $null = New-Item -ItemType Directory -Path $assetsPath +} + +$ColorScheme = @{ + Fill = '#4488ff' +} + +$FontStyle = [Ordered]@{ + FontFamily = 'monospace' + AlignmentBaseline = 'middle' + DominantBaseline = 'middle' +} + +$psChevron = + svg.symbol -Id psChevron -Content @( + svg.polygon -Points ( + @( + "40,20" + "45,20" + "60,50" + "35,80" + "32.5,80" + "55,50" + ) -join ' ' + ) -TransformOrigin '50% 50%' + ) -ViewBox 100, 100 + +foreach ($variant in '','animated','icon','animated-icon') { + $outputPath = if (-not $variant) { + Join-Path $assetsPath "ollama-powershell.svg" + } else { + Join-Path $assetsPath "ollama-powershell-$variant.svg" + } + +$llamaFill = [Ordered]@{ + fill = '#4488ff' + class = 'foreground-fill' +} + +$llamaStroke = [Ordered]@{ + fill = 'transparent' + stroke = '#4488ff' + class = 'foreground-stroke' + strokeWidth = '1%' +} + +$llamaOutline = [Ordered]@{ + comment = 'outline' + d = ' +M140.629 0.239929C132.66 1.52725 123.097 5.69568 116.354 10.845C95.941 26.3541 80.1253 59.2728 73.4435 100.283C70.9302 115.792 69.2138 137.309 69.2138 153.738C69.2138 173.109 71.4819 197.874 74.7309 214.977C75.4665 218.778 75.8343 222.15 75.5278 222.395C75.2826 222.64 72.2788 225.092 68.9072 227.789C57.3827 236.984 44.2029 251.145 35.1304 264.08C17.7209 288.784 6.44151 316.86 1.72133 347.265C-0.117698 359.28 -0.608106 383.555 0.863118 395.57C4.11207 423.278 12.449 446.695 26.7321 468.151L31.391 475.078L30.0424 477.346C20.4794 493.407 12.3264 516.64 8.52575 538.953C5.522 556.608 5.15419 561.328 5.15419 584.99C5.15419 608.837 5.4607 613.557 8.28054 630.047C11.6521 649.786 18.5178 670.689 26.1804 684.605C28.6938 689.141 34.8239 698.581 35.5595 699.072C35.8047 699.194 35.0691 701.462 33.9044 704.098C25.077 723.408 17.537 749.093 14.4106 770.733C12.2038 785.567 11.8973 790.349 11.8973 805.981C11.8973 825.903 13.0007 835.589 17.1692 851.466L17.7822 853.795H44.019H70.3172L68.6007 850.546C57.9957 830.93 57.0149 794.517 66.1487 758.166C70.3172 741.369 75.0374 729.048 83.8647 712.067L89.1366 701.769V695.455C89.1366 689.57 89.014 688.896 87.1137 685.034C85.6424 682.091 83.6808 679.578 80.1866 676.145C74.2404 670.383 69.9494 664.314 66.5165 656.835C51.4365 624.1 48.494 575.489 59.0991 534.049C63.5128 516.762 70.8076 501.376 78.4702 492.978C83.6808 487.215 86.378 480.779 86.378 474.097C86.378 467.17 83.926 461.469 78.4089 455.523C62.5932 438.604 52.8464 418.006 49.3522 394.038C44.3868 359.893 53.3981 322.683 73.8726 293.198C93.9181 264.263 122.055 245.689 153.503 240.724C160.552 239.559 173.732 239.743 181.088 241.092C189.119 242.502 194.145 242.072 199.295 239.62C205.67 236.617 208.858 232.877 212.597 224.295C215.907 216.633 218.482 212.464 225.409 203.821C233.746 193.461 241.776 186.411 254.649 177.89C269.362 168.266 286.097 161.278 302.771 157.906C308.839 156.68 311.659 156.496 323 156.496C334.341 156.496 337.161 156.68 343.229 157.906C367.688 162.872 391.964 175.5 411.335 193.399C415.503 197.261 425.495 209.644 428.683 214.794C429.909 216.816 432.055 221.108 433.403 224.295C437.142 232.877 440.33 236.617 446.705 239.62C451.671 242.011 456.881 242.502 464.605 241.214C476.804 239.13 486.183 239.314 498.137 241.766C538.841 249.98 574.273 283.512 589.966 328.446C603.636 367.862 599.774 409.118 579.422 440.626C575.989 445.96 572.556 450.251 567.591 455.523C556.863 466.986 556.863 481.208 567.53 492.978C585.062 512.165 596.035 559.367 592.724 600.99C590.518 628.453 583.468 653.035 573.782 666.95C572.066 669.402 568.511 673.57 565.813 676.145C562.319 679.578 560.358 682.091 558.886 685.034C556.986 688.896 556.863 689.57 556.863 695.455V701.769L562.135 712.067C570.963 729.048 575.683 741.369 579.851 758.166C588.863 794.027 588.066 829.704 577.767 849.995C576.909 851.711 576.173 853.305 576.173 853.489C576.173 853.673 587.882 853.795 602.226 853.795H628.218L628.892 851.159C629.26 849.75 629.873 847.604 630.179 846.378C630.854 843.681 632.202 835.712 633.306 828.049C634.348 820.325 634.348 791.881 633.306 783.299C629.383 752.158 622.823 727.454 612.096 704.098C610.931 701.462 610.195 699.194 610.44 699.072C610.747 698.888 612.463 696.436 614.302 693.677C627.666 673.448 635.88 648.008 640.049 614.415C641.152 605.158 641.152 565.374 640.049 556.485C637.106 533.559 633.551 517.988 627.666 502.234C625.214 495.675 618.716 481.821 615.958 477.346L614.609 475.078L619.268 468.151C633.551 446.695 641.888 423.278 645.137 395.57C646.608 383.555 646.118 359.28 644.279 347.265C639.497 316.798 628.279 288.845 610.87 264.08C601.797 251.145 588.617 236.984 577.093 227.789C573.721 225.092 570.717 222.64 570.472 222.395C570.166 222.15 570.534 218.778 571.269 214.977C578.687 176.296 578.441 128.053 570.656 90.3524C563.913 57.4951 551.653 31.3808 535.837 16.3008C523.209 4.28578 510.336 -0.863507 494.888 0.11731C459.456 2.20154 430.89 42.9667 419.61 107.21C417.771 117.57 416.178 129.708 416.178 133.018C416.178 134.305 415.932 135.347 415.626 135.347C415.319 135.347 412.929 134.121 410.354 132.589C383.014 116.405 352.608 107.762 323 107.762C293.392 107.762 262.986 116.405 235.646 132.589C233.071 134.121 230.681 135.347 230.374 135.347C230.068 135.347 229.822 134.305 229.822 133.018C229.822 129.585 228.167 117.08 226.39 107.21C216.152 49.5259 192.674 11.3354 161.472 1.71112C157.181 0.423799 144.982 -0.434382 140.629 0.239929ZM151.051 50.139C159.878 57.1273 169.686 77.1114 175.326 99.4863C176.368 103.532 177.471 108.191 177.778 109.907C178.023 111.563 178.697 115.302 179.249 118.183C181.64 131.179 182.743 145.217 182.866 162.32L182.927 179.178L178.697 185.43L174.468 191.744H164.598C153.074 191.744 141.61 193.216 130.637 196.158C126.714 197.139 122.913 198.12 122.178 198.304C121.013 198.549 120.829 198.181 120.155 193.154C116.538 165.875 116.722 135.654 120.707 110.52C125.12 82.5059 135.419 57.1273 145.472 49.6486C147.863 47.8708 148.292 47.9321 151.051 50.139ZM500.589 49.7098C506.658 54.1848 513.34 66.0772 518.305 81.2798C528.297 111.685 531.117 153.431 525.845 193.154C525.171 198.181 524.987 198.549 523.822 198.304C523.087 198.12 519.286 197.139 515.363 196.158C504.39 193.216 492.926 191.744 481.402 191.744H471.532L467.303 185.43L463.073 179.178L463.134 162.32C463.257 138.535 465.464 119.961 470.735 99.3024C476.314 77.1114 486.183 57.1273 494.949 50.139C497.708 47.9321 498.137 47.8708 500.589 49.7098Z +' +} + $llamaFill + +$llamaNose = [Ordered]@{ + Comment='nose' + d= ' +M313.498 358.237C300.195 359.525 296.579 360.015 290.203 361.303C279.843 363.448 265.989 368.23 256.365 372.95C222.895 389.317 199.846 416.596 192.796 448.166C191.386 454.419 191.202 456.503 191.202 467.047C191.202 477.468 191.386 479.736 192.735 485.682C202.114 526.938 240.12 557.405 289.284 562.983C299.95 564.148 346.049 564.148 356.715 562.983C396.193 558.508 430.154 537.114 445.418 507.076C449.463 499.046 451.425 493.835 453.264 485.682C454.613 479.736 454.797 477.468 454.797 467.047C454.797 456.503 454.613 454.419 453.203 448.166C442.965 402.313 398.461 366.207 343.903 359.341C336.792 358.483 318.157 357.747 313.498 358.237ZM336.424 391.585C354.631 393.547 372.96 400.045 387.672 409.853C395.58 415.125 406.737 426.159 411.518 433.393C417.403 442.342 420.774 451.476 422.307 462.572C422.981 467.66 422.614 471.522 420.774 479.736C417.893 491.996 408.943 504.808 396.867 513.758C391.227 517.865 379.519 523.812 372.347 526.141C358.738 530.493 349.849 531.29 318.095 531.045C297.376 530.861 293.697 530.677 287.751 529.574C267.461 525.773 251.4 517.681 239.753 505.36C230.312 495.429 226.021 486.357 223.692 471.706C222.65 464.901 224.611 453.622 228.596 444.12C233.439 432.534 245.944 418.129 258.327 409.853C272.671 400.29 291.552 393.486 308.9 391.647C315.582 390.911 329.742 390.911 336.424 391.585Z +' +} + $llamaFill + +$llamaNostril = [Ordered]@{ + Comment='nostril' + d=' +M299.584 436.336C294.925 438.849 291.676 445.224 292.657 449.944C293.76 455.032 298.235 460.182 305.223 464.412C308.963 466.68 309.208 466.986 309.392 469.254C309.514 470.603 309.024 474.465 308.35 477.898C307.614 481.269 307.062 484.825 307.062 485.806C307.124 488.442 309.576 492.733 312.15 494.817C314.419 496.656 314.848 496.717 321.223 496.901C327.047 497.085 328.273 496.962 330.602 495.859C336.61 492.916 338.142 487.522 335.935 477.162C334.096 468.519 334.464 467.17 339.062 464.534C343.904 461.714 349.054 456.749 350.586 453.377C353.529 446.941 350.831 439.646 344.333 436.274C342.74 435.477 340.778 435.11 337.897 435.11C333.422 435.11 330.541 436.152 325.269 439.523L322.265 441.424L320.365 440.259C312.58 435.661 311.17 435.11 306.449 435.171C303.078 435.171 301.239 435.477 299.584 436.336Z +' +} + $llamaStroke + +$llamaLeftEye = [Ordered]@{ + Comment='left eye' + d=' +M150.744 365.165C139.894 368.598 131.802 376.567 127.634 387.908C125.611 393.303 124.63 401.824 125.488 406.421C127.511 417.394 136.522 427.386 146.76 430.145C159.633 433.516 169.257 431.309 177.778 422.85C182.743 418.007 185.441 413.777 188.138 406.911C190.099 402.069 190.222 401.211 190.222 394.345L190.283 386.989L187.709 381.717C183.601 373.38 176.184 367.188 167.602 364.92C162.759 363.694 154.974 363.756 150.744 365.165Z +' +} + $llamaFill + +$llamaRightEye = [Ordered]@{ + Comment = 'Right Eye' + d = ' +M478.153 364.982C469.755 367.25 462.276 373.502 458.291 381.717L455.717 386.989L455.778 394.345C455.778 401.211 455.901 402.069 457.862 406.911C460.56 413.777 463.257 418.007 468.222 422.85C476.743 431.309 486.367 433.516 499.241 430.145C506.658 428.183 514.075 421.93 517.631 414.635C520.696 408.444 521.431 403.969 520.451 396.919C518.183 380.797 508.742 369.089 494.704 364.982C490.597 363.756 482.628 363.756 478.153 364.982Z +' +} + $llamaFill + + +$ollamaLlama = + SVG.symbol -Id llama -ViewBox 646,854 @( + SVG -Content @( + SVG.path @llamaOutline + SVG.path @llamaNose + SVG.path @llamaNostril -TransformOrigin '50% 50%' -Children @( + if ($variant -match 'animated') { + SVG.animateTransform -Type 'scale' -Values "1;.95;1" -RepeatCount 'indefinite' -Dur "$(60/40)s" -AttributeName transform -Additive 'sum' + } + ) + SVG.path @llamaLeftEye -Comment 'Left Eye' + SVG.path @llamaRightEye -Comment 'Right Eye' + ) + ) -TransformOrigin '50% 50%' + +svg -viewBox 300, 300 -Content @( + $psChevron + $ollamaLlama + svg.use -Href '#psChevron' @colorScheme -X 0% -Y 66% -Height 20% + svg.use -Href '#llama' @colorScheme -X 0% -Height 80% -Y 10% + # If the variant is not an icon, add the text + if ($variant -notmatch 'Icon') { + svg.text 'ollama-powershell' -FontFamily 'monospace' -AlignmentBaseline 'middle' -TextAnchor middle -X 50% -Y 85% @colorScheme -FontSize 1em + } +) -Class 'foreground-fill' -OutputPath ( + $outputPath +) + +} \ No newline at end of file diff --git a/Build/ollama-powershell.GitHubWorkflow.PSDevOps.ps1 b/Build/ollama-powershell.GitHubWorkflow.PSDevOps.ps1 new file mode 100644 index 0000000..fb1c821 --- /dev/null +++ b/Build/ollama-powershell.GitHubWorkflow.PSDevOps.ps1 @@ -0,0 +1,15 @@ +#requires -Module PSDevOps +Import-BuildStep -SourcePath ( + Join-Path $PSScriptRoot 'GitHub' +) -BuildSystem GitHubWorkflow + +Push-Location ($PSScriptRoot | Split-Path) +New-GitHubWorkflow -Name "Build ollama-powershell" -On Push, + PullRequest, + Demand -Job TestPowerShellOnLinux, + TagReleaseAndPublish, BuildOllamaPowerShell -Environment ([Ordered]@{ + REGISTRY = 'ghcr.io' + IMAGE_NAME = '${{ github.repository }}' + }) -OutputPath .\.github\workflows\BuildOllamaPowerShell.yml + +Pop-Location \ No newline at end of file diff --git a/Build/ollama-powershell.ezout.ps1 b/Build/ollama-powershell.ezout.ps1 new file mode 100644 index 0000000..480ccb0 --- /dev/null +++ b/Build/ollama-powershell.ezout.ps1 @@ -0,0 +1,39 @@ +#requires -Module EZOut +# Install-Module EZOut or https://github.com/StartAutomating/EZOut +$myFile = $MyInvocation.MyCommand.ScriptBlock.File +$myRoot = $myFile | Split-Path | Split-Path +$myModuleName = $myFile | Split-Path | Split-Path | Split-Path -Leaf +Push-Location $myRoot +$formatting = @( + # Add your own Write-FormatView here, + # or put them in a Formatting or Views directory + foreach ($potentialDirectory in 'Formatting','Views','Types') { + Join-Path $myRoot $potentialDirectory | + Get-ChildItem -ea ignore | + Import-FormatView -FilePath {$_.Fullname} + } +) + +$destinationRoot = $myRoot + +if ($formatting) { + $myFormatFilePath = Join-Path $destinationRoot "$myModuleName.format.ps1xml" + # You can also output to multiple paths by passing a hashtable to -OutputPath. + $formatting | Out-FormatData -Module $MyModuleName -OutputPath $myFormatFilePath +} + +$types = @( + # Add your own Write-TypeView statements here + # or declare them in the 'Types' directory + Join-Path $myRoot Types | + Get-Item -ea ignore | + Import-TypeView + +) + +if ($types) { + $myTypesFilePath = Join-Path $destinationRoot "$myModuleName.types.ps1xml" + # You can also output to multiple paths by passing a hashtable to -OutputPath. + $types | Out-TypeData -OutputPath $myTypesFilePath +} +Pop-Location diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ca2e753 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Code of Conduct + +We have a simple subjective code of conduct: + +1. Be Respectful +2. Be Helpful +3. Do No Harm + +Failure to follow the code of conduct may result in blocks or banishment. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e223d8d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# Contributing + +## Who Can Contribute? + +We welcome all contributions, as long as they're related to using ollama and PowerShell. + +## What Can I Contribute? + +You can contribute issues and pull requests containing new PowerShell functions or documentation. + +We welcome integrations with any AI APIs and tools out there. + +## Why Contribute? + +You should contribute to help the community, and share your knowledge. + +This module is meant to be a grab bag of AI fun in PowerShell, +and your contribution can bring more joy and knowledge to the world. \ No newline at end of file diff --git a/Commands/Get-Ollama.ps1 b/Commands/Get-Ollama.ps1 new file mode 100644 index 0000000..da509b9 --- /dev/null +++ b/Commands/Get-Ollama.ps1 @@ -0,0 +1,653 @@ +function Get-Ollama { + <# + .SYNOPSIS + Gets Ollama models and responses. + .DESCRIPTION + Gets [Ollama](https://ollama.com/) models and responses. + + This wraps the [Ollama API](https://github.com/ollama/ollama/blob/main/docs/api.md) in PowerShell. + + This allows you to ask any number of AI models questions and get responses. + .EXAMPLE + Get-Ollama -Model "llama3.2" -Prompt "What is the meaning of life, the universe, and everything?" + .EXAMPLE + Get-Ollama -Model "phi4" -Prompt "Write me a limerick" + .EXAMPLE + # Pull down a model from the Ollama hub. Please enjoy the progress bars. + Get-Ollama -Model "tinyllama" -Pull + .EXAMPLE + Get-Ollama -Model "llama3.2" -Prompt "Ollama is 22 years old and is busy saving the world. Respond using JSON" -Format ([Ordered]@{ + type = 'object' + properties = [Ordered]@{ + age = @{type="integer"} + available = @{type="boolean"} + job = @{type="string"} + } + required = @("age","available") + }) -NoStream + .LINK + https://github.com/ollama/ollama/blob/main/docs/api.md + #> + [CmdletBinding(PositionalBinding=$false, DefaultParameterSetName='cli',SupportsShouldProcess)] + param( + # Any arguments to ollama. + # This allows you to use Get-Ollama as a proxy for the ollama CLI. + # This is the default parameter set. + [Parameter(ParameterSetName='cli',ValueFromRemainingArguments)] + [PSObject[]] + $ArgumentList, + + # Any input object. + [Parameter(ValueFromPipeline)] + [PSObject] + $InputObject, + + # If set, will list the currently loaded models + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/tags')] + [Alias('ListModels')] + [switch] + $ListModel, + + # If set, will get the ollama version. + [Parameter(Mandatory,ParameterSetName='/version')] + [switch] + $Version, + + # The name of the language model. + # If this is not provided, it will be set to the last model used. + # If no last model was used, it will be set to `llama3.2` + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/show')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/generate')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/chat')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/pull')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/create')] + [Alias('Model','LanguageModel')] + [string] + $ModelName, + + # If set, will not stream the response + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/generate')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/chat')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/pull')] + [Alias('NoStreaming')] + [switch] + $NoStream, + + # Any options to ollama. These are used in the `/generate` and `/chat` endpoints. + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/generate')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/chat')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/embeddings')] + [Alias('Options')] + [PSObject] + $Option = [Ordered]@{}, + + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/generate')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/chat')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/embeddings')] + [int] + $Seed, + + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/generate')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/chat')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/embeddings')] + [int] + $Temperature, + + + # When creating a new model, this is the name of the base model + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/create')] + [string] + $From, + + # The prompt to send to the model. + [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='/generate')] + [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='/embeddings')] + [string] + $Prompt, + + # The object or system prompt used to create a new model. + [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='/create')] + [PSObject] + $Create, + + # One or more messages to send to the model. + [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='/chat')] + [Alias('Messages','Chat','ChatHistory')] + [PSObject[]] + $Message, + + # If set, will pull a model from the Ollama hub + [Parameter(Mandatory,ParameterSetName='/pull')] + [switch] + $Pull, + + # If set, will get the embedding for a prompt + [Parameter(Mandatory,ParameterSetName='/embeddings')] + [Alias('Embeddings')] + [switch] + $Embedding, + + # The format for responses. + # This should be a JSON schema. + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/chat')] + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/generate')] + [Alias('ObjectFormat','Schema')] + [PSObject] + $Format, + + # If set, will list the running models + [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='/ps')] + [Alias('ListProcesses','GetProcesses','RunningModels')] + [switch] + $RunningModel, + + # The url to the Ollama API. + [Parameter(ValueFromPipelineByPropertyName)] + [uri] + $OllamaApi = "http://$([IPAddress]::Loopback):11434/api" + ) + + begin { + filter StreamResponse { + $in = $_ + $initalProperties = [Ordered]@{} + $typenames = @(foreach ($arg in $args) { + if ($arg -is [string]) { $arg} + if ($arg -is [Collections.IDictionary]) { + try { + $initalProperties += $arg + } catch { + foreach ($kv in $arg.GetEnumerator()) { + $initalProperties[$kv.Key] = $kv.Value + } + } + } + }) + $in.TypeName = $typenames + $jobName = "$($in.uri)" + $in.MainRunspace = [runspace]::DefaultRunspace + foreach ($kv in $initalProperties.GetEnumerator()) { + $in[$kv.Key] = $kv.Value + } + $startedThreadJob = Start-ThreadJob -ScriptBlock { + param([Collections.IDictionary]$io) + foreach ($ioKeyValue in $io.GetEnumerator()) { + $ExecutionContext.SessionState.PSVariable.Set($ioKeyValue.Key,$ioKeyValue.Value) + } + $io.StringBuilder = [Text.StringBuilder]::new() + $in = $io + $webRequest = [net.httpwebrequest]::Create($in.Uri) + if ($in.Method) { + $webRequest.Method = $in.Method + } + if ($in.body) { + $bytes = $OutputEncoding.GetBytes($in.body) + $webRequest.GetRequestStream().Write( + $bytes, 0, $bytes.Length + ) + } + $webResponse = $webRequest.GetResponse() + if (-not $webResponse) { + return + } + $responseStream = $webResponse.GetResponseStream() + $responseStreamReader = [IO.StreamReader]::new($responseStream) + $startTime = [datetime]::Now + $responseNumber = 0 + + while ($readLine = $responseStreamReader.ReadLine()) { + $streamingResponse = $readLine | ConvertFrom-Json + foreach ( + $thingToAppend in $streamingResponse.error, + $streamingResponse.message.content, + $streamingResponse.response + ) { + if ($thingToAppend) { + $null = $io.StringBuilder.Append($thingToAppend) + } + } + $streamingResponse.pstypenames.clear() + foreach ($typename in $typenames) { + $streamingResponse.pstypenames.add($typename) + } + if ($Prompt -and -not $streamingResponse.Prompt) { + $streamingResponse.psobject.properties.add( + [psnoteproperty]::new('Prompt',$Prompt) + ) + } + $streamingResponse.psobject.properties.add( + [psnoteproperty]::new('ResponseNumber',$responseNumber) + ) + + $streamingResponse + $responseNumber++ + } + } -ArgumentList $in -Name $jobName + $startedThreadJob.psobject.properties.add([psnoteproperty]::new('IO',$in)) + $startedThreadJob.pstypenames.add('Ollama.Job') + $startedThreadJob + } + + filter WaitAndSummarize { + $inJob = $_ + $typenames = @($args) + if ($inJob -isnot [Management.Automation.Job]) { + return + } + + $progressSplat = [Ordered]@{Id = $inJob.Id} + while ($inJob.JobStateInfo.State -eq 'NotStarted') { + Start-Sleep -Milliseconds (Get-Random -Maximum 100 -Minimum 10) + } + + $lastLength = 0 + + while ("$($inJob.JobStateInfo.State)" -and $inJob.JobStateInfo.State -notin 'Completed','Failed') { + $resultsSoFar = @($inJob | Receive-Job -Keep) + if ($inJob.IO.StringBuilder.Length) { + $progressSplat.Activity = "$( + if ($Prompt) { + $prompt + } elseif ($chat -and $chat[-1] -is [string]) { + $chat[-1] + } elseif ($chat -and $chat[-1].content) { + $chat[-1].content + } + ) " + $progressCharWidth = if ($Host.UI.RawUI.BufferSize.Width) { $Host.UI.RawUI.BufferSize.Width / 2 } else { 60 } + $progressSplat.Status = "$( + $inJob.IO.StringBuilder.ToString().Substring( + [Math]::Max( + 0, + $inJob.IO.StringBuilder.Length - $progressCharWidth + ) + ) -replace '[\s\n\r]', ' ' + )" + Write-Progress @progressSplat + $lastLength = $inJob.IO.StringBuilder.Length + } + + if ($resultsSoFar.total -and $resultsSoFar.completed) { + for ($lastIndex = $resultsSoFar.Count - 1; $lastIndex -ge 0; $lastIndex--) { + if ($resultsSoFar[$lastIndex].completed) { + $gbDown = [Math]::Round($resultsSoFar[$lastIndex].completed / 1GB, 2) + $gbTotal = [Math]::Round($resultsSoFar[$lastIndex].total / 1GB, 2) + $progressSplat.Activity = "$($resultsSoFar[$lastIndex].status) " + $progressSplat.PercentComplete = [Math]::Round( + $resultsSoFar[$lastIndex].completed * 100 / $resultsSoFar[$lastIndex].total, + 2 + ) + $progressSplat.Status = "$($modelName) [${gbDown}gb / ${gbTotal}gb] $($progressSplat.PercentComplete)%" + Write-Progress @progressSplat + break + } + } + + } + Start-Sleep -Milliseconds (Get-Random -Maximum 1kb -Minimum .25kb) + } + $progressSplat.Activity = 'Completed!' + $progressSplat.Status = 'Done!' + Write-Progress @progressSplat -Activity 'Waiting for Completion' -Status 'all done' -Completed + + foreach ($typename in $typenames) { + $inJob.pstypenames.insert(0,$typename) + } + + if ($originalConsolePosition) { + [console]::Write("`e[$($originalConsolePosition.Item2);$($originalConsolePosition.Item1)H") + $inJob + } else { + $inJob + } + + + } + + $ollamaCli = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ollama','Application') + $nonPipelineParameters = [Ordered]@{} + $PSBoundParameters + } + + process { + # Derive the URL from the parameter set + $parameterSet = $PSCmdlet.ParameterSetName + $in = $_ + if ($in.pstypenames -contains 'Ollama.Model') { + if ($parameterSet -eq 'cli') { + $parameterSet = '/show' + } + if (-not $nonPipelineParameters['ModelName']) { + if ($in.Model) { + $PSBoundParameters['ModelName'] = $modelName = $in.Model + } elseif ($in.ModelName) { + $PSBoundParameters['ModelName'] = $modelName = $in.ModelName + } + } + } + $invokeSplat = [Ordered]@{ + Uri = $OllamaApi, ($parameterSet -replace '^/') -join '/' + } + + Write-Verbose "$($invokeSplat.Uri)" + + + + # Determine the model name. + # This won't _always_ be important, but in most scenarios it is. + $modelName = + # If we had not provided a model name parameter + if (-not $psBoundParameters['ModelName']) { + # default to the last model name used + if ($script:LastOllamaModelName) { + $script:LastOllamaModelName + } else { + # If there is no last model name, default to `llama3.2` + 'llama3.2' + } + } else { + # If there was already a model name provided, use it. + $ModelName + + + } + + if ($PSBoundParameters['Format']) { + if ($format -is [Collections.IList]) { + $requiredNames = @() + $FixedFormat = [Ordered]@{ + type = 'object' + properties = [Ordered]@{} + } + foreach ($formatProperty in $format) { + if ($formatProperty -is [string]) { + $fixedFormat.properties[$formatProperty] = @{type='string'} + $requiredNames+= $formatProperty + } elseif ($formatProperty -is [Collections.IDictionary]) { + foreach ($formatKeyValue in $formatProperty.GetEnumerator()) { + $fixedFormat.properties[$formatKeyValue.Key] = @{type=$formatKeyValue.Value} + $requiredNames+= $formatKeyValue.Key + } + } + } + $FixedFormat.required = $requiredNames + $format = $FixedFormat + } + } + + + # Switch things based off the parameter set + switch ($parameterSet) { + # cli is the only non-restful parameter set + "cli" { + if (-not $ollamaCli) { + Write-Error 'Missing Ollama' + return + } + if ($MyInvocation.InvocationName -ne $MyInvocation.MyCommand.Name) { + $argumentList = @("run", $MyInvocation.InvocationName) + $argumentList + } + if ($Format -and -not ($ArgumentList -contains '--format')) { + $argumentList += @("--format", $Format) + } + & $ollamaCli @ArgumentList + } + { + $Seed -or $Temperature + } { + if ($Seed) { + # If we have a seed, set the appropriate option + $Option['seed'] = $Seed + } + + if ($Temperature) { + # If we have a temperature, set the appropriate option + $Option['temperature'] = $Temperature + } + } + # version is the easiest parameter set + "/version" { + # If `-WhatIf` was passed, return the splat. + if ($WhatIfPreference) { return $invokeSplat } + # If -Confirm was passed and the user does not want to continue, return. + if (-not $PSCmdlet.ShouldProcess($invokeSplat.Uri)) { return } + + Invoke-RestMethod @invokeSplat + } + # /tags and /ps are functionally the same + { $_ -in '/tags','/ps' } { + # If `-WhatIf` was passed, return the splat. + if ($WhatIfPreference) { return $invokeSplat } + # If -Confirm was passed and the user does not want to continue, return. + if (-not $PSCmdlet.ShouldProcess($invokeSplat.Uri)) { return } + + # Get the .models property from any rest method call + foreach ($modelInfo in @(@(Invoke-RestMethod @invokeSplat).models)) { + # (keep moving past any nulls) + if (-not $modelInfo) { continue } + # Clear our typenames + $modelInfo.pstypenames.clear() + # If we're in the `/ps` parameter set, add `Ollama.Model.Running` + if ($parameterSet -eq '/ps') { + $modelInfo.pstypenames.add("Ollama.Model.Running") + } + # always add `Ollama.Model` + $modelInfo.pstypenames.add('Ollama.Model') + # and emit the model information. + $modelInfo + } + } + # /show gets us model details + '/show' { + $invokeSplat.Method = 'POST' + $invokeSplat.Body = ConvertTo-Json -InputObject @{model=$ModelName;verbose= $VerbosePreference -eq 'continue'} + # If `-WhatIf` was passed, return the splat. + if ($WhatIfPreference) { return $invokeSplat } + # If -Confirm was passed and the user does not want to continue, return. + if (-not $PSCmdlet.ShouldProcess($invokeSplat.Uri, $invokeSplat.Body -join [Environment]::NewLine)) { return } + # Get the model information. + $modelInfo = Invoke-RestMethod @invokeSplat + # assuming we got something back + if ($modelInfo) { + # Clear our typenames + $modelInfo.pstypenames.clear() + # And decorate the model information + $modelInfo.pstypenames.add('Ollama.Model.Info') + $modelInfo.pstypenames.add('Ollama.Model') + if (-not $modelInfo.model) { + $modelInfo.psobject.Properties.Add( + [psnoteproperty]::new('Model',$ModelName), + $true + ) + } + $modelInfo + } + } + # /chat describes a conversation with a model, and is a bit more complex + '/chat' { + $invokeSplat.Method = 'POST' + $invokeSplat.Body = [Ordered]@{ + model = $ModelName + messages = # since we want to be easier than the raw API + # walk over each message and make it more system friendly. + @(foreach ($msg in $message) { + if ($msg -is [string]) { + # If it was a string, make it a `user` message + @{role='user';content=$msg} + } else { + # otherwise, keep the message as-is. + $msg + } + }) + } + + # If we want output in a particular format + if ($Format) { + # add it to the body + $invokeSplat.Body.format = $Format + } + + # If we do not want to stream the response + if ($NoStream) { + # say so now. + $invokeSplat.Body.stream = $false + } + + # If we have any additional options + if ($Option) { + # add them to the body. + $invokeSplat.Body.options = $Option + } + + # Convert the body to JSON. + $invokeSplat.Body = $invokeSplat.Body | ConvertTo-Json -Depth 10 + # If `-WhatIf` was passed, return the splat. + if ($WhatIfPreference) { return $invokeSplat } + # If -Confirm was passed and the user does not want to continue, return. + if (-not $PSCmdlet.ShouldProcess($invokeSplat.Uri, $invokeSplat.Body -join [Environment]::NewLine)) { return } + + # If we're not streaming + if ($NoStream) { + # we can use Invoke-RestMethod + $noStreamingResponse = Invoke-RestMethod @invokeSplat + # and simply decorate our return. + $noStreamingResponse.pstypenames.clear() + $noStreamingResponse.pstypenames.add('Ollama.Chat') + $noStreamingResponse + } else { + # Otherwise, we need to stream the response + $InvokeSplat | + # each returned message will be an `Ollama.Chat.Response` + StreamResponse 'Ollama.Chat.Response' | + # and we will wait for the job to finish and call it both an `Ollama.Chat` and an `Ollama.Job` + WaitAndSummarize 'Ollama.Chat' 'Ollama.Job' + } + } + # /pull will download a model from the Ollama hub + '/pull' { + $invokeSplat.Method = 'POST' + $invokeSplat.Body = [Ordered]@{model=$ModelName} + + # We can technically pull without streaming, but, why would you want to? + if ($NoStream) { + $invokeSplat.Body.stream = $false + } + + $invokeSplat.Body = $invokeSplat.Body | ConvertTo-Json -Depth 20 + # If `-WhatIf` was passed, return the splat. + if ($WhatIfPreference) { return $invokeSplat } + # If -Confirm was passed and the user does not want to continue, return. + if (-not $PSCmdlet.ShouldProcess($invokeSplat.Uri, $invokeSplat.Body -join [Environment]::NewLine)) { return } + + # If we are not streaming + if ($NoStream) { + # then try to Invoke-RestMethod + # (for a pull, there's a decent chance this would time out) + $noStreamingResponse = Invoke-RestMethod @invokeSplat + $noStreamingResponse.pstypenames.clear() + $noStreamingResponse.pstypenames.add('Ollama.Pull') + $noStreamingResponse + } else { + # Otherwise, we need to stream the response + $InvokeSplat | + # each returned message will be an `Ollama.Pull.Response` + StreamResponse 'Ollama.Pull.Response' | + # and we will wait for the job to finish and call it both an `Ollama.Pull` and an `Ollama.Job` + WaitAndSummarize 'Ollama.Pull' 'Ollama.Job' + } + } + '/create' { + $invokeSplat.Method = 'POST' + $invokeSplat.Body = [Ordered]@{} + if ($Create -is [string]) { + $invokeSplat.Body.system = $create + } else { + if ($create -is [Collections.IDictionary]) { + foreach ($property in $create.psobject.properties) { + $invokeSplat.Body[$property.Name] = $property.Value + } + } else { + foreach ($property in $create.psobject.properties) { + $invokeSplat.Body[$property.Name] = $property.Value + } + } + } + if (-not $body.model) { + $invokeSplat.Body.model = $ModelName + } + if ($From) { + $invokeSplat.Body.from = $From + } + + $invokeSplat.Body = $invokeSplat.Body | ConvertTo-Json -Depth 20 + if ($WhatIfPreference) { + return $invokeSplat + } + if (-not $PSCmdlet.ShouldProcess($invokeSplat.Uri, $invokeSplat.Body -join [Environment]::NewLine)) { return } + + if ($NoStream) { + $noStreamingResponse = Invoke-RestMethod @invokeSplat + $noStreamingResponse.pstypenames.clear() + $noStreamingResponse.pstypenames.add('Ollama.Create') + $noStreamingResponse + } else { + $InvokeSplat | + StreamResponse 'Ollama.Create.Response' | + WaitAndSummarize 'Ollama.Create' + } + } + '/embeddings' { + $invokeSplat.Method = 'POST' + $invokeSplat.Body = [Ordered]@{ + model=$ModelName + prompt=$Prompt + } + + if ($Option) { + $invokeSplat.Body.options = $Option + } + $invokeSplat.Body = $invokeSplat.Body | ConvertTo-Json -Depth 20 + if ($WhatIfPreference) { + return $invokeSplat + } + if (-not $PSCmdlet.ShouldProcess($invokeSplat.Uri, $invokeSplat.Body -join [Environment]::NewLine)) { return } + Invoke-RestMethod @invokeSplat + } + '/generate' { + $invokeSplat.Method = 'POST' + $invokeSplat.Body = [Ordered]@{ + model=$ModelName + prompt=$Prompt + } + + if ($NoStream) { + $invokeSplat.Body.stream = $false + } + + if ($Option) { + $invokeSplat.Body.options = $Option + } + + if ($Format) { + $invokeSplat.Body.format = $Format + } + + $invokeSplat.Body = $invokeSplat.Body | ConvertTo-Json -Depth 20 + if ($WhatIfPreference) { + return $invokeSplat + } + if (-not $PSCmdlet.ShouldProcess($invokeSplat.Uri, $invokeSplat.Body -join [Environment]::NewLine)) { return } + + if ($NoStream) { + $noStreamingResponse = Invoke-RestMethod @invokeSplat + $noStreamingResponse.pstypenames.clear() + $noStreamingResponse.pstypenames.add('Ollama.Prompt') + $noStreamingResponse + } else { + $InvokeSplat | + StreamResponse 'Ollama.Prompt.Response' | + WaitAndSummarize 'Ollama.Prompt' 'Ollama.Job' + } + } + } + } +} diff --git a/Commands/Remove-Ollama.ps1 b/Commands/Remove-Ollama.ps1 new file mode 100644 index 0000000..97d3fe6 --- /dev/null +++ b/Commands/Remove-Ollama.ps1 @@ -0,0 +1,47 @@ +function Remove-Ollama { + <# + .SYNOPSIS + Remove a model + .DESCRIPTION + Removes an Ollama model. + + This is a destructive operation, and will confirm before proceeding. + .LINK + https://github.com/ollama/ollama/blob/main/docs/api.md#delete-a-model + #> + [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] + param( + # The name of the model to remove. + [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='/delete')] + [Alias('Model','LanguageModel')] + [string] + $ModelName, + + # The url to the Ollama API. + [Parameter(ValueFromPipelineByPropertyName)] + [uri] + $OllamaApi = "http://$([ipaddress]::Loopback):11434/api" + ) + + process { + $parameterSet = $PSCmdlet.ParameterSetName + $invokeSplat = [Ordered]@{ + Uri = $OllamaApi, $parameterSet -join '/' -replace '/{2,}','/' -replace ':/','://' + } + Write-Verbose "$($invokeSplat.Uri)" + + switch ($parameterSet) { + '/delete' { + $invokeSplat.Method = 'DELETE' + $invokeSplat.Body = @{model = $ModelName} | ConvertTo-Json + if ($WhatIfPreference) { + return $invokeSplat + } + if (-not $PSCmdlet.ShouldProcess("Delete model $modelName")) { + return + } + Invoke-RestMethod @invokeSplat + } + } + } +} diff --git a/README.md b/README.md index 85f8128..298810f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,104 @@ -# ollama-powershell -Ollama PowerShell: Play with AI in PowerShell +
+ollama-powershell + + + +
+❤️ + +
+ +Play with AI in PowerShell + + +# Getting Started + +## Installing ollama-powershell + +You can install ollama-powershell from the [PowerShell Gallery](https://www.powershellgallery.com/packages/ollama-powershell/) + +~~~PowerShell +Install-Module -Name ollama-powershell +~~~ + +To force an update, use the `-Force` + +~~~PowerShell +Install-Module -Name ollama-powershell -Force +~~~ + +## Importing ollama-powershell + +Once ollama-powershell is installed, you can import it by name: + +~~~PowerShell +Import-Module -Name ollama-powershell -PassThru +~~~ + +Once it is loaded, you can get commands from ollama-powershell with Get-Command + +~~~PowerShell +Get-Command -Module ollama-powershell +~~~ + +You can show the syntax of all commands with `Get-Command -Syntax` + +~~~PowerShell +Get-Command -Module ollama-powershell -Syntax +~~~ + +You can get help about a command with `Get-Help` + +~~~PowerShell +Get-Help Get-Ollama +~~~ + +You can show examples for a command with the -Examples + +~~~PowerShell +Get-Help Get-Ollama -Examples +~~~ + +## Using ollama-powershell + +There are a few ways to use ollama-powershell: + +Without any parameters, or with unmapped input, `Get-Ollama` will run the `ollama` cli + +~~~PowerShell +Get-Ollama +~~~ + +With PowerShell style parameters, `Get-Ollama` will wrap the [ollama api](https://github.com/ollama/ollama/blob/main/docs/api.md) + +### Get-Ollama -Pull + +Let's pull down a model: + +~~~PowerShell +Get-Ollama -Pull -Model 'tinyllama' +~~~ + +### Get-Ollama -Chat + +And now let's ask it something: + +~~~PowerShell +Get-Ollama -Model 'tinyllama' -Chat 'Why is the sky blue?','Limit your response to three sentences or less.' +~~~ + +### Calling a model by name + +If you have a model installed, you can refer to that model by name. + +This is effectively a shortcut to `ollama run $ModelName` + +~~~PowerShell +tinyllama "What are you?" +~~~ + +If you just downloaded a model and this does not work, re-import ollama-powershell with: + +~~~PowerShell +Import-Module ollama-powershell -Force +~~~ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d5d1620 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +# Security + +We take security seriously. If you believe you have discovered a vulnerability, please [file an issue](https://github.com/StartAutomating/AI/issues). + +## Special Security Considerations + +AI is not inherantly dangerous, but what comes out of them might well be. + +In order to avoid data poisoning attacks, please _never_ directly run any code from the internet that you do not trust. + +Please also assume all WebSockets are untrustworthy. + +There are a few easy ways to do this. + +AI responses should never: + +1. Be piped into `Invoke-Expression` +2. Be expanded with `.ExpandString` +3. Be directly placed into a `SQL` query + +## Ethical AI considerations + +AI should be used ethically. In the case of PowerShell, this can be especially tricky. + +PowerShell gives you the ability to deal with vast amounts of information, and significant existing automation capabilities. + +With power comes responsbility, and all users of this module are morally (and potentially legally) responsible for the potential harms that callous use of AI can inflict. + +If you are aware of ethical misuse of the product, please [file an issue](https://github.com/StartAutomating/AI/issues) + +Sunlight is often the best disenfectant. \ No newline at end of file diff --git a/Types/Ollama.Job/Ollama.Job.format.ps1 b/Types/Ollama.Job/Ollama.Job.format.ps1 new file mode 100644 index 0000000..357326b --- /dev/null +++ b/Types/Ollama.Job/Ollama.Job.format.ps1 @@ -0,0 +1,30 @@ +Write-FormatView -TypeName Ollama.Job -Action { + $inJob = $_ + if ($injob.StringBuilder.Length) { + "$($inJob.StringBuilder)" + } else { + $jobResults = $_ | Receive-Job -Keep *>&1 + $resultText = @(foreach ($result in $jobResults) { + if ($result.response) { + $result.response + } elseif ($result.message.content) { + $result.message.content + } + elseif ($result.error) { + if ($PSStyle) { + $PSStyle.Formatting.Error + $result.error + $PSStyle.Reset + } + } + elseif ($result -is [Management.Automation.ErrorRecord]) { + $result.Exception.Message + } + }) -join '' + if ($resultText) { + $resultText + } else { + $jobResults | Out-String + } + } +} diff --git a/Types/Ollama.Job/ToString.ps1 b/Types/Ollama.Job/ToString.ps1 new file mode 100644 index 0000000..bf317b6 --- /dev/null +++ b/Types/Ollama.Job/ToString.ps1 @@ -0,0 +1,5 @@ +if ($this.IO.StringBuilder){ + "$($this.IO.StringBuilder)" +} else { + return ($this | Receive-Job -Keep | Out-String) +} \ No newline at end of file diff --git a/Types/Ollama.Model/Ollama.Model.format.ps1 b/Types/Ollama.Model/Ollama.Model.format.ps1 new file mode 100644 index 0000000..d493e91 --- /dev/null +++ b/Types/Ollama.Model/Ollama.Model.format.ps1 @@ -0,0 +1,13 @@ +Write-FormatView -TypeName Ollama.Model -Property Model, Modified_At, Size -VirtualProperty @{ + Size = { + if ($_.Size -gt 1GB) { + '{0:N2} gb' -f ($_.Size / 1GB) + } elseif ($_.Size -gt 1MB) { + '{0:N2} mb' -f ($_.Size / 1MB) + } elseif ($_.Size -gt 1KB) { + '{0:N2} kb' -f ($_.Size / 1KB) + } elseif ($_.Size) { + '{0:N2} b' -f $_.Size + } + } +} diff --git a/ollama-powershell.format.ps1xml b/ollama-powershell.format.ps1xml new file mode 100644 index 0000000..0c64516 --- /dev/null +++ b/ollama-powershell.format.ps1xml @@ -0,0 +1,93 @@ + + + + + Ollama.Job + + Ollama.Job + + + + + + + + $inJob = $_ + if ($injob.StringBuilder.Length) { + "$($inJob.StringBuilder)" + } else { + $jobResults = $_ | Receive-Job -Keep *>&1 + $resultText = @(foreach ($result in $jobResults) { + if ($result.response) { + $result.response + } elseif ($result.message.content) { + $result.message.content + } + elseif ($result.error) { + if ($PSStyle) { + $PSStyle.Formatting.Error + $result.error + $PSStyle.Reset + } + } + elseif ($result -is [Management.Automation.ErrorRecord]) { + $result.Exception.Message + } + }) -join '' + if ($resultText) { + $resultText + } else { + $jobResults | Out-String + } + } + + + + + + + + + Ollama.Model + + Ollama.Model + + + + + + + + + + + + + + + + Model + + + Modified_At + + + + if ($_.Size -gt 1GB) { + '{0:N2} gb' -f ($_.Size / 1GB) + } elseif ($_.Size -gt 1MB) { + '{0:N2} mb' -f ($_.Size / 1MB) + } elseif ($_.Size -gt 1KB) { + '{0:N2} kb' -f ($_.Size / 1KB) + } elseif ($_.Size) { + '{0:N2} b' -f $_.Size + } + + + + + + + + + \ No newline at end of file diff --git a/ollama-powershell.ps.psm1 b/ollama-powershell.ps.psm1 new file mode 100644 index 0000000..5663d22 --- /dev/null +++ b/ollama-powershell.ps.psm1 @@ -0,0 +1,39 @@ +$commandsPath = Join-Path $PSScriptRoot Commands +[include('*-*')]$commandsPath + +$myModule = $MyInvocation.MyCommand.ScriptBlock.Module +$ExecutionContext.SessionState.PSVariable.Set($myModule.Name, $myModule) +$myModule.pstypenames.insert(0, $myModule.Name) + +New-PSDrive -Name $MyModule.Name -PSProvider FileSystem -Scope Global -Root $PSScriptRoot -ErrorAction Ignore + +if ($home) { + $MyModuleProfileDirectory = Join-Path ([Environment]::GetFolderPath("LocalApplicationData")) $MyModule.Name + if (-not (Test-Path $MyModuleProfileDirectory)) { + $null = New-Item -ItemType Directory -Path $MyModuleProfileDirectory -Force + } + New-PSDrive -Name "My$($MyModule.Name)" -PSProvider FileSystem -Scope Global -Root $MyModuleProfileDirectory -ErrorAction Ignore +} + +# Set a script variable of this, set to the module +# (so all scripts in this scope default to the correct `$this`) +$script:this = $myModule + +#region Custom +$ollamaApplication = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ollama','Application') +if (-not $ollamaApplication) { + Write-Warning "Ollama is not installed or in the path. Please install it from https://ollama.com/download" +} else { + $isOllamaRunning = Get-Process -Name ollama -ErrorAction Ignore + if (-not $isOllamaRunning) { + $ollamaServer = Start-ThreadJob -Name "ollama serve" -ScriptBlock { ollama serve } + } + $script:OllamaModels = Get-Ollama -ListModel + foreach ($modelInfo in $script:OllamaModels) { + $ExecutionContext.SessionState.PSVariable.Set("alias:$($modelInfo.Name -replace '\:latest$')", 'Get-Ollama') + } +} +#endregion Custom + +Export-ModuleMember -Alias * -Function * -Variable $myModule.Name + diff --git a/ollama-powershell.psd1 b/ollama-powershell.psd1 new file mode 100644 index 0000000..8d6b0d9 --- /dev/null +++ b/ollama-powershell.psd1 @@ -0,0 +1,27 @@ +@{ + RootModule = 'ollama-powershell.psm1' + ModuleVersion = '0.0.1' + GUID = 'e9b68160-0f70-4821-86c5-64ddb66e841c' + Author = 'James Brundage' + CompanyName = 'Start-Automating' + Copyright = '2025 Start-Automating' + TypesToProcess = @('ollama-powershell.types.ps1xml') + FormatsToProcess = @('ollama-powershell.format.ps1xml') + PrivateData = @{ + PSData = @{ + Tags = @('AI','ollama', 'PowerShell', 'LLM') + LicenseURI = 'https://github.com/StartAutomating/ollama-powershell/blob/main/LICENSE' + ProjectURI = 'https://github.com/StartAutomating/ollama-powershell' + ReleaseNotes = @' +Initial release of ollama-powershell! + +Use ollama-powershell to interact with the Ollama API and run LLM models locally. + +* `Get-Ollama` wraps the Ollama CLI and Ollama rest API +* `Remove-Ollama` removes models from your local system + +'@ + } + } +} + diff --git a/ollama-powershell.psm1 b/ollama-powershell.psm1 new file mode 100644 index 0000000..8f40702 --- /dev/null +++ b/ollama-powershell.psm1 @@ -0,0 +1,49 @@ +$commandsPath = Join-Path $PSScriptRoot Commands +:ToIncludeFiles foreach ($file in (Get-ChildItem -Path "$commandsPath" -Filter "*-*" -Recurse)) { + if ($file.Extension -ne '.ps1') { continue } # Skip if the extension is not .ps1 + foreach ($exclusion in '\.[^\.]+\.ps1$') { + if (-not $exclusion) { continue } + if ($file.Name -match $exclusion) { + continue ToIncludeFiles # Skip excluded files + } + } + . $file.FullName +} + +$myModule = $MyInvocation.MyCommand.ScriptBlock.Module +$ExecutionContext.SessionState.PSVariable.Set($myModule.Name, $myModule) +$myModule.pstypenames.insert(0, $myModule.Name) + +New-PSDrive -Name $MyModule.Name -PSProvider FileSystem -Scope Global -Root $PSScriptRoot -ErrorAction Ignore + +if ($home) { + $MyModuleProfileDirectory = Join-Path ([Environment]::GetFolderPath("LocalApplicationData")) $MyModule.Name + if (-not (Test-Path $MyModuleProfileDirectory)) { + $null = New-Item -ItemType Directory -Path $MyModuleProfileDirectory -Force + } + New-PSDrive -Name "My$($MyModule.Name)" -PSProvider FileSystem -Scope Global -Root $MyModuleProfileDirectory -ErrorAction Ignore +} + +# Set a script variable of this, set to the module +# (so all scripts in this scope default to the correct `$this`) +$script:this = $myModule + +#region Custom +$ollamaApplication = $ExecutionContext.SessionState.InvokeCommand.GetCommand('ollama','Application') +if (-not $ollamaApplication) { + Write-Warning "Ollama is not installed or in the path. Please install it from https://ollama.com/download" +} else { + $isOllamaRunning = Get-Process -Name ollama -ErrorAction Ignore + if (-not $isOllamaRunning) { + $ollamaServer = Start-ThreadJob -Name "ollama serve" -ScriptBlock { ollama serve } + } + $script:OllamaModels = Get-Ollama -ListModel + foreach ($modelInfo in $script:OllamaModels) { + $ExecutionContext.SessionState.PSVariable.Set("alias:$($modelInfo.Name -replace '\:latest$')", 'Get-Ollama') + } +} +#endregion Custom + +Export-ModuleMember -Alias * -Function * -Variable $myModule.Name + + diff --git a/ollama-powershell.types.ps1xml b/ollama-powershell.types.ps1xml new file mode 100644 index 0000000..40bece2 --- /dev/null +++ b/ollama-powershell.types.ps1xml @@ -0,0 +1,23 @@ + + + + Ollama.Job + + + ToString + + + + + + Ollama.Model + + + + \ No newline at end of file