first commit
Some checks failed
Docker Build and Push / build-and-push (push) Has been cancelled
Tests / test (bash) (push) Has been cancelled
Tests / test (zsh) (push) Has been cancelled
Tests / lint (push) Has been cancelled
Tests / docker (push) Has been cancelled

This commit is contained in:
2025-12-02 09:02:03 +01:00
commit 24d36cbad4
27 changed files with 2781 additions and 0 deletions

14
.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
.git
.github
.gitignore
.idea
*.md
!README.md
!LICENSE
tests
run_tests.sh
debian
homebrew
docker-compose.yml
Dockerfile.test
.dockerignore

66
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Docker Build and Push
on:
push:
tags:
- 'v*'
branches:
- main
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Build
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push test image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.test
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test
cache-from: type=gha
cache-to: type=gha,mode=max

80
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get version from tag
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Create release tarball
run: |
mkdir -p dist
tar -czf dist/finish-v${{ steps.get_version.outputs.VERSION }}.tar.gz \
--exclude='.git' \
--exclude='.github' \
--exclude='dist' \
--exclude='.idea' \
--transform "s,^,finish-${{ steps.get_version.outputs.VERSION }}/," \
.
- name: Calculate SHA256
id: sha256
run: |
SHA256=$(sha256sum dist/finish-v${{ steps.get_version.outputs.VERSION }}.tar.gz | awk '{print $1}')
echo "SHA256=$SHA256" >> $GITHUB_OUTPUT
echo "SHA256: $SHA256"
- name: Build Debian package
run: |
sudo apt-get update
sudo apt-get install -y debhelper devscripts
dpkg-buildpackage -us -uc -b
mv ../finish_*.deb dist/ || true
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
dist/*.tar.gz
dist/*.deb
body: |
## Installation
### Quick Install (Linux/macOS)
```bash
curl -sSL https://git.appmodel.nl/Tour/finish/main/docs/install.sh | bash
```
### Homebrew (macOS)
Update the formula with SHA256: `${{ steps.sha256.outputs.SHA256 }}`
### Debian/Ubuntu
Download the `.deb` file and install:
```bash
sudo dpkg -i finish_*.deb
sudo apt-get install -f # Install dependencies
```
### Docker
```bash
docker pull ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION }}
```
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for details.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

69
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shell: [bash, zsh]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y bash zsh curl jq bc bash-completion bats
- name: Make script executable
run: chmod +x finishte.sh
- name: Run BATS tests
run: |
if [ -d tests ]; then
bats tests/
else
echo "No tests directory found"
fi
- name: Test installation
run: |
export HOME=$PWD/test-home
mkdir -p $HOME/.local/bin
cp finishte.sh $HOME/.local/bin/finishte
chmod +x $HOME/.local/bin/finishte
$HOME/.local/bin/finishte --help
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run shellcheck
run: shellcheck finishte.sh docs/install.sh || true
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t finish:test .
- name: Test Docker image
run: |
docker run --rm finish:test -c "finishte --help"

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# Build artifacts
debian/finish/
debian/.debhelper/
debian/files
debian/*.log
debian/*.substvars
*.deb
*.tar.gz
*.tar.xz
# IDEs
.idea/
.vscode/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Environment
.env
.env.local
# Logs and cache
finish.log
~/.finish/
*.cache
__pycache__/
# Temporary files
*.tmp
temp/
dist/

7
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.10.0
hooks:
- id: shellcheck
files: ^.+\.sh$

52
Dockerfile Normal file
View File

@@ -0,0 +1,52 @@
# finishte.sh Docker Image
FROM ubuntu:22.04
LABEL maintainer="finish contributors"
LABEL description="LLM-powered command-line autocompletion with LM-Studio integration"
LABEL version="0.5.0"
# Prevent interactive prompts during installation
ENV DEBIAN_FRONTEND=noninteractive
# Install dependencies
RUN apt-get update && \
apt-get install -y \
bash \
bash-completion \
curl \
wget \
jq \
bc \
vim \
git \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create application directory
WORKDIR /opt/finish
# Copy application files
COPY finish.sh /usr/bin/finish
COPY README.md LICENSE ./
# Make script executable
RUN chmod +x /usr/bin/finish
# Create finish directory
RUN mkdir -p /root/.finish
# Source bash-completion in .bashrc
RUN echo "" >> /root/.bashrc && \
echo "# Bash completion" >> /root/.bashrc && \
echo "if [ -f /etc/bash_completion ] && ! shopt -oq posix; then" >> /root/.bashrc && \
echo " . /etc/bash_completion" >> /root/.bashrc && \
echo "fi" >> /root/.bashrc
# Set working directory to home
WORKDIR /root
# Set entrypoint to bash
ENTRYPOINT ["/bin/bash"]
# Default command shows help
CMD ["-c", "echo 'finish.sh Docker Container' && echo '' && finish --help && exec /bin/bash"]

34
Dockerfile.test Normal file
View File

@@ -0,0 +1,34 @@
# finish.sh Test Container
FROM ubuntu:22.04
LABEL maintainer="finish contributors"
LABEL description="Test environment for finish.sh"
ENV DEBIAN_FRONTEND=noninteractive
# Install dependencies including BATS for testing
RUN apt-get update && \
apt-get install -y \
bash \
bash-completion \
curl \
wget \
jq \
bc \
vim \
git \
bats \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /opt/finish
# Copy all files
COPY . .
# Make scripts executable
RUN chmod +x finish.sh
# Run tests by default
ENTRYPOINT ["bats"]
CMD ["tests"]

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2025, finish contributors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

365
PUBLISHING.md Normal file
View File

@@ -0,0 +1,365 @@
# Publishing Guide for finishte.sh
This guide explains how to publish and distribute finishte.sh through various channels.
## Overview
finishte.sh can be distributed through multiple channels:
1. **Direct installation script** - One-line curl install
2. **Debian/Ubuntu packages** - APT repository
3. **Homebrew** - macOS package manager
4. **Docker** - Container images
5. **GitHub Releases** - Direct downloads
## Prerequisites
Before publishing, ensure:
- [ ] All tests pass (`./run_tests.sh`)
- [ ] Version numbers are updated in all files
- [ ] CHANGELOG.md is updated
- [ ] Documentation is complete and accurate
## Version Management
Update version numbers in these files:
1. `finishte.sh` - Line 34: `export ACSH_VERSION=0.5.0`
2. `debian/changelog` - Add new entry
3. `homebrew/finish.rb` - Line 6: `version "0.5.0"`
4. `docs/install.sh` - Line 7: `ACSH_VERSION="v0.5.0"`
5. `Dockerfile` - Line 6: `LABEL version="0.5.0"`
## 1. Direct Installation Script
The install script at `docs/install.sh` enables one-line installation:
```bash
curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash
```
### Setup:
- Host the script on a reliable server or GitHub/GitLab
- Ensure the URL is accessible and supports HTTPS
- Test the installation on clean systems
### Testing:
```bash
# Test in Docker
docker run --rm -it ubuntu:22.04 bash
curl -sSL YOUR_URL/install.sh | bash
```
## 2. Debian/Ubuntu Packages
### Building the Package
```bash
# Install build tools
sudo apt-get install debhelper devscripts
# Build the package
dpkg-buildpackage -us -uc -b
# This creates:
# ../finish_0.5.0-1_all.deb
```
### Testing the Package
```bash
# Install locally
sudo dpkg -i ../finish_*.deb
sudo apt-get install -f # Fix dependencies
# Test installation
finishte --help
finishte install
```
### Creating an APT Repository
#### Option A: Using Packagecloud (Recommended for beginners)
1. Sign up at https://packagecloud.io
2. Create a repository
3. Upload your .deb file:
```bash
gem install package_cloud
package_cloud push yourname/finish/ubuntu/jammy ../finish_*.deb
```
Users install with:
```bash
curl -s https://packagecloud.io/install/repositories/yourname/finish/script.deb.sh | sudo bash
sudo apt-get install finish
```
#### Option B: Using GitHub Pages + reprepro
1. Create a repository structure:
```bash
mkdir -p apt-repo/{conf,pool,dists}
```
2. Create `apt-repo/conf/distributions`:
```
Origin: finishte.sh
Label: finish
Codename: stable
Architectures: all
Components: main
Description: APT repository for finishte.sh
```
3. Add packages:
```bash
reprepro -b apt-repo includedeb stable ../finish_*.deb
```
4. Host on GitHub Pages or your server
Users install with:
```bash
echo "deb https://your-domain.com/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/finish.list
curl -fsSL https://your-domain.com/apt-repo/key.gpg | sudo apt-key add -
sudo apt-get update
sudo apt-get install finish
```
## 3. Homebrew Formula
### Creating a Homebrew Tap
1. Create a GitHub repository: `homebrew-finish`
2. Add the formula to `Formula/finish.rb`
3. Create a release tarball and calculate SHA256:
```bash
git archive --format=tar.gz --prefix=finish-0.5.0/ v0.5.0 > finish-0.5.0.tar.gz
sha256sum finish-0.5.0.tar.gz
```
4. Update the formula with the correct URL and SHA256
5. Test the formula:
```bash
brew install --build-from-source ./homebrew/finish.rb
brew test finish
brew audit --strict finish
```
### Publishing the Tap
Users can install with:
```bash
brew tap yourname/finish
brew install finish
```
Or directly:
```bash
brew install yourname/finish/finish
```
## 4. Docker Images
### Building Images
```bash
# Production image
docker build -t finish:0.5.0 -t finish:latest .
# Test image
docker build -f Dockerfile.test -t finish:test .
```
### Publishing to GitHub Container Registry
The GitHub Actions workflow automatically publishes Docker images when you push a tag:
```bash
git tag -a v0.5.0 -m "Release v0.5.0"
git push origin v0.5.0
```
Images will be available at:
```
ghcr.io/appmodel/finish:0.5.0
ghcr.io/appmodel/finish:latest
```
### Publishing to Docker Hub
```bash
# Login
docker login
# Tag and push
docker tag finish:latest yourname/finish:0.5.0
docker tag finish:latest yourname/finish:latest
docker push yourname/finish:0.5.0
docker push yourname/finish:latest
```
## 5. GitHub Releases
### Automated Release Process
The `.github/workflows/release.yml` workflow automatically:
1. Creates release artifacts when you push a tag
2. Builds Debian packages
3. Publishes Docker images
4. Creates a GitHub release with downloads
### Manual Release Process
1. Create a release tarball:
```bash
git archive --format=tar.gz --prefix=finish-0.5.0/ v0.5.0 > finish-0.5.0.tar.gz
```
2. Create release on GitHub:
- Go to Releases → Draft a new release
- Tag: `v0.5.0`
- Title: `finishte.sh v0.5.0`
- Upload: tarball and .deb file
- Write release notes
## Release Checklist
Before releasing a new version:
- [ ] Update all version numbers
- [ ] Update CHANGELOG.md
- [ ] Run all tests
- [ ] Test installation script
- [ ] Build and test Debian package
- [ ] Test Docker images
- [ ] Update documentation
- [ ] Create git tag
- [ ] Push tag to trigger CI/CD
- [ ] Verify GitHub release created
- [ ] Verify Docker images published
- [ ] Update Homebrew formula with new SHA256
- [ ] Test installation from all sources
- [ ] Announce release (if applicable)
## Distribution URLs
After publishing, users can install from:
**Quick Install:**
```bash
curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash
```
**Homebrew:**
```bash
brew tap appmodel/finish
brew install finish
```
**APT (if configured):**
```bash
sudo apt-get install finish
```
**Docker:**
```bash
docker pull ghcr.io/appmodel/finish:latest
```
**Direct Download:**
```
https://git.appmodel.nl/Tour/finish/releases/latest
```
## Troubleshooting
### Debian Package Issues
**Problem:** Package won't install
```bash
# Check dependencies
dpkg-deb -I finish_*.deb
# Test installation
sudo dpkg -i finish_*.deb
sudo apt-get install -f
```
**Problem:** Lintian warnings
```bash
lintian finish_*.deb
# Fix issues in debian/ files
```
### Homebrew Issues
**Problem:** Formula audit fails
```bash
brew audit --strict ./homebrew/finish.rb
# Fix issues reported
```
**Problem:** Installation fails
```bash
brew install --verbose --debug ./homebrew/finish.rb
```
### Docker Issues
**Problem:** Build fails
```bash
docker build --no-cache -t finish:test .
```
**Problem:** Image too large
```bash
# Use multi-stage builds
# Remove unnecessary files
# Combine RUN commands
```
## Continuous Integration
The project includes three GitHub Actions workflows:
1. **test.yml** - Runs on every push/PR
- Runs BATS tests
- Tests installation
- Runs shellcheck
- Tests Docker builds
2. **docker.yml** - Builds and publishes Docker images
- Triggered by tags and main branch
- Publishes to GitHub Container Registry
3. **release.yml** - Creates releases
- Triggered by version tags
- Builds all artifacts
- Creates GitHub release
- Publishes packages
## Support and Documentation
- Update the README.md with installation instructions
- Maintain CHANGELOG.md with version history
- Create issue templates for bug reports and feature requests
- Set up discussions for community support
## Security Considerations
- Always use HTTPS for installation scripts
- Sign Debian packages with GPG
- Use checksums for release artifacts
- Regularly update dependencies
- Monitor for security vulnerabilities
## Next Steps
After initial publication:
1. Monitor GitHub issues for user feedback
2. Set up analytics if desired
3. Create documentation site
4. Announce on relevant forums/communities
5. Consider submitting to package manager directories

300
README.md Normal file
View File

@@ -0,0 +1,300 @@
# finishte.sh
Command-line completion powered by local LLMs. Press `Tab` twice and get intelligent suggestions based on your terminal context.
## Quick Start
### One-Line Install (Recommended)
```bash
curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash
```
### Manual Installation
```bash
git clone https://git.appmodel.nl/Tour/finish.git finish
cd finish
./finish.sh install
source ~/.bashrc
```
### Other Installation Methods
See [Installation](#installation) below for package managers, Docker, and more options.
## What It Does
finishte.sh enhances your terminal with context-aware command suggestions. It analyzes:
- Current directory and recent files
- Command history
- Environment variables
- Command help output
Then generates relevant completions using a local LLM.
## Features
**Local by Default**
Runs with LM-Studio on localhost. No external API calls, no data leaving your machine.
**Context-Aware**
Understands what you're doing based on your environment and history.
**Cached**
Stores recent queries locally for instant responses.
**Flexible**
Switch between different models and providers easily.
## Configuration
View current settings:
```bash
finishte config
```
Change the endpoint or model:
```bash
finishte config set endpoint http://192.168.1.100:1234/v1/chat/completions
finishte config set model your-model-name
```
Select a model interactively:
```bash
finishte model
```
## Usage
Type a command and press `Tab` twice:
```bash
docker <TAB><TAB>
git commit <TAB><TAB>
ffmpeg <TAB><TAB>
```
Natural language works too:
```bash
# find large files <TAB><TAB>
# compress to zip <TAB><TAB>
```
Test it without executing:
```bash
finishte command "your command"
finishte command --dry-run "your command"
```
## Installation
### Quick Install (Linux/macOS)
The fastest way to get started:
```bash
curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash
source ~/.bashrc # or ~/.zshrc for zsh
finishte model
```
### Package Managers
#### Homebrew (macOS)
```bash
brew tap appmodel/finish
brew install finish
finishte install
source ~/.bashrc
```
#### APT (Debian/Ubuntu)
Download the `.deb` package from [releases](https://git.appmodel.nl/Tour/finish/releases):
```bash
sudo dpkg -i finish_*.deb
sudo apt-get install -f # Install dependencies
finishte install
source ~/.bashrc
```
### Docker
Run finishte.sh in a container:
```bash
# Build the image
docker build -t finish .
# Run interactively
docker run -it finish
# Or use docker-compose
docker-compose up -d finish
docker-compose exec finish bash
```
Inside the container:
```bash
finishte install
source ~/.bashrc
finishte model # Configure your LLM endpoint
```
### From Source
```bash
git clone https://git.appmodel.nl/Tour/finish.git finish
cd finish
chmod +x finish.sh
sudo ln -s $PWD/finish.sh /usr/local/bin/finish
finishte install
source ~/.bashrc
```
## Requirements
- Bash 4.0+ or Zsh 5.0+
- curl
- jq
- bc
- bash-completion (recommended)
- LM-Studio or Ollama (for LLM inference)
## Directory Structure
```
~/.finishte/
├── config # Configuration file
├── finishte.log # Usage log
└── cache/ # Cached completions
```
## Commands
```bash
finishte install # Set up finishte
finishte remove # Uninstall
finishte config # Show/edit configuration
finishte model # Select model
finishte enable # Enable completions
finishte disable # Disable completions
finishte clear # Clear cache and logs
finishte usage # Show usage statistics
finishte system # Show system information
finishte --help # Show help
```
## Providers
Currently supports:
- **LM-Studio** (default) - Local models via OpenAI-compatible API
- **Ollama** - Local models via Ollama API
Add custom providers by editing `finishte.sh` and adding entries to `_finishte_modellist`.
## Development
### Local Development
Clone and link for development:
```bash
git clone https://git.appmodel.nl/Tour/finish.git finish
cd finish
ln -s $PWD/finishte.sh $HOME/.local/bin/finish
finishte install
```
### Running Tests
```bash
# Install BATS testing framework
sudo apt-get install bats # Ubuntu/Debian
# Run tests
./run_tests.sh
# Or use Docker
docker build -f Dockerfile.test -t finish:test .
docker run --rm finish:test
```
### Building Packages
#### Debian Package
```bash
# Install build dependencies
sudo apt-get install debhelper devscripts
# Build the package
dpkg-buildpackage -us -uc -b
# Package will be created in parent directory
ls ../*.deb
```
#### Docker Images
```bash
# Production image
docker build -t finish:latest .
# Test image
docker build -f Dockerfile.test -t finish:test .
# Using docker-compose
docker-compose build
```
## Distribution & Publishing
### Creating a Release
1. Update version in `finishte.sh` (ACSH_VERSION)
2. Update version in `debian/changelog`
3. Update version in `homebrew/finish.rb`
4. Create and push a git tag:
```bash
git tag -a v0.5.0 -m "Release version 0.5.0"
git push origin v0.5.0
```
This triggers the GitHub Actions workflow which:
- Builds release artifacts
- Creates Debian packages
- Publishes Docker images to GitHub Container Registry
- Creates a GitHub release with downloadable assets
### Homebrew Formula
After creating a release, update the Homebrew formula:
1. Calculate SHA256 of the release tarball:
```bash
curl -sL https://git.appmodel.nl/Tour/finish/archive/v0.5.0.tar.gz | sha256sum
```
2. Update `homebrew/finish.rb` with the new SHA256 and version
3. Submit to your Homebrew tap or the main Homebrew repository
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
BSD 2-Clause License. See LICENSE file.

201
SUMMARY.md Normal file
View File

@@ -0,0 +1,201 @@
# Publication Setup Summary
This document summarizes the publication setup completed for finishte.sh.
## Files Created
### 1. Installation Script
- **`docs/install.sh`** - Enhanced one-line installer with:
- Dependency checking
- Shell detection (bash/zsh)
- Color-coded output
- Error handling
- PATH management
- Usage: `curl -sSL <URL>/install.sh | bash`
### 2. Debian Package Structure
Complete Debian packaging in `debian/` directory:
- **`control`** - Package metadata and dependencies
- **`rules`** - Build instructions
- **`changelog`** - Version history
- **`copyright`** - License information
- **`compat`** - Debhelper compatibility level
- **`postinst`** - Post-installation script
- **`source/format`** - Package format specification
Build command: `dpkg-buildpackage -us -uc -b`
### 3. Homebrew Formula
- **`homebrew/finish.rb`** - Homebrew formula for macOS
- Includes dependencies, installation steps, and caveats
- Update SHA256 after creating releases
### 4. Docker Configuration
- **`Dockerfile`** - Production container image
- **`Dockerfile.test`** - Testing container with BATS
- **`docker-compose.yml`** - Multi-service Docker setup
- **`.dockerignore`** - Excludes unnecessary files from builds
### 5. GitHub Actions Workflows
CI/CD automation in `.github/workflows/`:
- **`test.yml`** - Runs tests on every push/PR
- **`release.yml`** - Creates releases and builds packages
- **`docker.yml`** - Builds and publishes Docker images
### 6. Documentation
- **`PUBLISHING.md`** - Comprehensive publishing guide
- **`README.md`** - Updated with installation methods
- **`.gitignore`** - Updated to exclude build artifacts
## Distribution Channels
### 1. Quick Install (Recommended)
```bash
curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash
```
### 2. Debian/Ubuntu Package
Users can install the `.deb` file:
```bash
sudo dpkg -i finish_*.deb
sudo apt-get install -f
```
Or from an APT repository (when configured):
```bash
sudo apt-get install finish
```
### 3. Homebrew (macOS)
```bash
brew tap closedloop-technologies/finish
brew install finish
```
### 4. Docker
```bash
docker pull ghcr.io/closedloop-technologies/finish:latest
docker run -it ghcr.io/closedloop-technologies/finish:latest
```
### 5. From Source
```bash
git clone https://git.appmodel.nl/Tour/finish.git
cd finish
./finishte.sh install
```
## Next Steps
### Immediate Actions
1. **Test the installation script** on clean systems
2. **Build and test the Debian package**:
```bash
dpkg-buildpackage -us -uc -b
sudo dpkg -i ../finish_*.deb
```
3. **Test Docker builds**:
```bash
docker build -t finish:test .
docker run -it finish:test
```
### For First Release
1. **Update version numbers** in:
- `finishte.sh` (line 34)
- `debian/changelog`
- `homebrew/finish.rb`
- `docs/install.sh`
- `Dockerfile`
2. **Create release tarball**:
```bash
git archive --format=tar.gz --prefix=finish-0.5.0/ v0.5.0 > finish-0.5.0.tar.gz
```
3. **Calculate SHA256** for Homebrew:
```bash
sha256sum finish-0.5.0.tar.gz
```
4. **Create and push tag**:
```bash
git tag -a v0.5.0 -m "Release v0.5.0"
git push origin v0.5.0
```
This triggers GitHub Actions to build everything automatically.
### Setting Up Package Repositories
#### APT Repository Options:
1. **Packagecloud** (easiest):
- Sign up at https://packagecloud.io
- Upload .deb file
- Users get one-line install
2. **GitHub Pages** (free):
- Use `reprepro` to create repository
- Host on GitHub Pages
- Users add your repository to sources
See `PUBLISHING.md` for detailed instructions.
#### Homebrew Tap:
1. Create GitHub repo: `homebrew-finish`
2. Add formula to `Formula/finish.rb`
3. Users install with: `brew tap yourname/finish && brew install finish`
## Testing Checklist
Before releasing:
- [ ] Test install script on Ubuntu 22.04
- [ ] Test install script on Debian 12
- [ ] Test install script on macOS
- [ ] Build Debian package successfully
- [ ] Install and test Debian package
- [ ] Build Docker image successfully
- [ ] Run BATS tests in Docker
- [ ] Test Homebrew formula (if applicable)
- [ ] Verify all version numbers match
- [ ] Test GitHub Actions workflows
## Automation
The GitHub Actions workflows handle:
- **On every push/PR**: Run tests, lint code, build Docker images
- **On version tag push**: Build packages, create release, publish Docker images
This means once set up, releasing a new version is as simple as:
```bash
git tag -a v0.5.1 -m "Release v0.5.1"
git push origin v0.5.1
```
Everything else happens automatically!
## Support Channels
Consider setting up:
- GitHub Issues for bug reports
- GitHub Discussions for Q&A
- Documentation website (GitHub Pages)
- Discord/Slack community (optional)
## Resources
- Debian Packaging Guide: https://www.debian.org/doc/manuals/maint-guide/
- Homebrew Formula Cookbook: https://docs.brew.sh/Formula-Cookbook
- GitHub Actions Documentation: https://docs.github.com/en/actions
- Docker Best Practices: https://docs.docker.com/develop/dev-best-practices/
## Conclusion
Your project is now fully set up for publication with:
- ✅ One-line installation script
- ✅ Debian/Ubuntu package support
- ✅ Homebrew formula for macOS
- ✅ Docker containers
- ✅ Automated CI/CD with GitHub Actions
- ✅ Comprehensive documentation
All distribution channels are ready. Follow the "Next Steps" section above to test and publish your first release!

140
USAGE.md Normal file
View File

@@ -0,0 +1,140 @@
# Usage Guide
## Installation
Install and set up finishte:
```bash
finishte install
source ~/.bashrc
```
## Configuration
View current settings:
```bash
finishte config
```
Change settings:
```bash
finishte config set temperature 0.5
finishte config set endpoint http://localhost:1234/v1/chat/completions
finishte config set model your-model-name
```
Reset to defaults:
```bash
finishte config reset
```
## Model Selection
Select a model interactively:
```bash
finishte model
```
Use arrow keys to navigate, Enter to select, or 'q' to quit.
## Basic Commands
Show help:
```bash
finishte --help
```
Test completions without caching:
```bash
finishte command "your command here"
```
Preview the prompt sent to the model:
```bash
finishte command --dry-run "your command here"
```
View system information:
```bash
finishte system
```
## Usage Statistics
Check your usage stats and costs:
```bash
finishte usage
```
## Cache Management
Clear cached completions and logs:
```bash
finishte clear
```
## Enable/Disable
Temporarily disable:
```bash
finishte disable
```
Re-enable:
```bash
finishte enable
```
## Uninstallation
Remove finishte completely:
```bash
finishte remove
```
## Using finishte
Once enabled, just type a command and press `Tab` twice to get suggestions:
```bash
git <TAB><TAB>
docker <TAB><TAB>
find <TAB><TAB>
```
Natural language also works:
```bash
# convert video to mp4 <TAB><TAB>
# list all processes using port 8080 <TAB><TAB>
# compress this folder to tar.gz <TAB><TAB>
```
## Configuration File
The config file is located at `~/.finishte/config` and contains:
- `provider`: Model provider (lmstudio, ollama)
- `model`: Model name
- `temperature`: Response randomness (0.0 - 1.0)
- `endpoint`: API endpoint URL
- `api_prompt_cost`: Cost per input token
- `api_completion_cost`: Cost per output token
- `max_history_commands`: Number of recent commands to include in context
- `max_recent_files`: Number of recent files to include in context
- `cache_dir`: Directory for cached completions
- `cache_size`: Maximum number of cached items
- `log_file`: Path to usage log file

10
debian/changelog vendored Normal file
View File

@@ -0,0 +1,10 @@
finish (0.5.0-1) stable; urgency=medium
* Initial Debian package release
* LLM-powered bash and zsh finishtion
* Support for LM-Studio and Ollama providers
* Context-aware command suggestions
* Intelligent caching system
* Privacy-focused local operation
-- finishte.sh Contributors <noreply@finishte.sh> Mon, 02 Dec 2024 12:00:00 +0000

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
10

24
debian/control vendored Normal file
View File

@@ -0,0 +1,24 @@
Source: finish
Section: utils
Priority: optional
Maintainer: finishte.sh Contributors <noreply@finishte.sh>
Build-Depends: debhelper (>= 10)
Standards-Version: 4.5.0
Homepage: https://git.appmodel.nl/Tour/finish/finish
Vcs-Git: https://git.appmodel.nl/Tour/finish.git
Package: finish
Architecture: all
Depends: bash (>= 4.0) | zsh (>= 5.0), curl, jq, bc, bash-completion
Recommends: lm-studio | ollama
Description: LLM-powered command-line finishtion
finishte.sh enhances your terminal with context-aware command suggestions
using local large language models. Features include:
.
* Local-first operation with LM-Studio or Ollama
* Context-aware suggestions based on command history and environment
* Intelligent caching for instant responses
* Support for multiple LLM providers
* Privacy-focused - no data leaves your machine by default
.
Simply press Tab twice to get intelligent command suggestions powered by AI.

30
debian/copyright vendored Normal file
View File

@@ -0,0 +1,30 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: finish
Upstream-Contact: https://git.appmodel.nl/Tour/finish/finish
Source: https://git.appmodel.nl/Tour/finish
Files: *
Copyright: 2024-2025 finishte.sh Contributors
License: BSD-2-Clause
License: BSD-2-Clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

27
debian/postinst vendored Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/sh
set -e
case "$1" in
configure)
echo "finishte.sh has been installed successfully!"
echo ""
echo "To complete setup, run:"
echo " finishte install"
echo ""
echo "Then reload your shell:"
echo " source ~/.bashrc # for bash"
echo " source ~/.zshrc # for zsh"
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

19
debian/rules vendored Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
%:
dh $@
override_dh_auto_install:
install -D -m 0755 finishte.sh $(CURDIR)/debian/finish/usr/bin/finishte
install -D -m 0644 LICENSE $(CURDIR)/debian/finish/usr/share/doc/finish/copyright
install -D -m 0644 README.md $(CURDIR)/debian/finish/usr/share/doc/finish/README.md
override_dh_auto_build:
# Nothing to build
override_dh_auto_test:
# Tests run in CI

1
debian/source/format vendored Normal file
View File

@@ -0,0 +1 @@
3.0 (native)

30
docker-compose.yml Normal file
View File

@@ -0,0 +1,30 @@
version: '3.8'
services:
finish:
build:
context: .
dockerfile: Dockerfile
image: finish:latest
container_name: finish
stdin_open: true
tty: true
volumes:
- finish-data:/root/.finish
environment:
- ACSH_ENDPOINT=http://host.docker.internal:1234/v1/chat/completions
extra_hosts:
- "host.docker.internal:host-gateway"
command: /bin/bash
finish-test:
build:
context: .
dockerfile: Dockerfile.test
image: finish:test
container_name: finish-test
volumes:
- .:/opt/finish
volumes:
finish-data:

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
finishte

198
docs/install.sh Normal file
View File

@@ -0,0 +1,198 @@
#!/bin/sh
# finishte.sh installer
# Usage: curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash
set -e
ACSH_VERSION="v0.5.0"
BRANCH_OR_VERSION=${1:-main}
REPO_URL="https://git.appmodel.nl/Tour/finish/raw/branch"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo_green() {
printf "${GREEN}%s${NC}\n" "$1"
}
echo_error() {
printf "${RED}ERROR: %s${NC}\n" "$1" >&2
}
echo_warning() {
printf "${YELLOW}WARNING: %s${NC}\n" "$1"
}
# Detect the current shell
detect_shell() {
if [ -n "$ZSH_VERSION" ]; then
echo "zsh"
elif [ -n "$BASH_VERSION" ]; then
echo "bash"
else
parent_shell=$(ps -p $PPID -o comm= 2>/dev/null || echo "")
case "$parent_shell" in
*zsh*) echo "zsh" ;;
*bash*) echo "bash" ;;
*) echo "unknown" ;;
esac
fi
}
# Check dependencies
check_dependencies() {
local missing=""
if ! command -v curl > /dev/null 2>&1 && ! command -v wget > /dev/null 2>&1; then
missing="${missing}curl or wget, "
fi
if ! command -v jq > /dev/null 2>&1; then
missing="${missing}jq, "
fi
if ! command -v bc > /dev/null 2>&1; then
missing="${missing}bc, "
fi
if [ -n "$missing" ]; then
echo_error "Missing dependencies: ${missing%, }"
echo ""
echo "Install them with:"
echo " Ubuntu/Debian: sudo apt-get install curl jq bc bash-completion"
echo " CentOS/RHEL: sudo yum install curl jq bc bash-completion"
echo " macOS: brew install jq bc bash-completion"
return 1
fi
return 0
}
# Download file
download_file() {
local url="$1"
local output="$2"
if command -v curl > /dev/null 2>&1; then
curl -fsSL "$url" -o "$output"
elif command -v wget > /dev/null 2>&1; then
wget -q -O "$output" "$url"
else
echo_error "Neither curl nor wget is available"
return 1
fi
}
# Main installation
main() {
echo_green "=========================================="
echo_green " finishte.sh Installation"
echo_green " Version: $ACSH_VERSION"
echo_green "=========================================="
echo ""
# Detect shell
SHELL_TYPE=$(detect_shell)
case "$SHELL_TYPE" in
zsh)
SCRIPT_NAME="finishte.zsh"
RC_FILE="$HOME/.zshrc"
;;
bash)
SCRIPT_NAME="finishte.sh"
RC_FILE="$HOME/.bashrc"
;;
*)
echo_error "Unsupported shell. Currently only bash and zsh are supported."
exit 1
;;
esac
echo "Detected shell: $SHELL_TYPE"
# Check dependencies
echo "Checking dependencies..."
if ! check_dependencies; then
exit 1
fi
echo_green "✓ All dependencies found"
echo ""
# Determine installation location
if [ -d "$HOME/.local/bin" ]; then
INSTALL_LOCATION="$HOME/.local/bin/finishte"
# Add to PATH if not already there
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
echo_warning "$HOME/.local/bin is not in PATH"
if [ ! -f "$RC_FILE" ] || ! grep -q ".local/bin" "$RC_FILE"; then
echo "Adding $HOME/.local/bin to PATH in $RC_FILE"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$RC_FILE"
fi
fi
elif [ -w "/usr/local/bin" ]; then
INSTALL_LOCATION="/usr/local/bin/finishte"
else
echo_error "Cannot write to /usr/local/bin and $HOME/.local/bin doesn't exist"
echo "Please create $HOME/.local/bin or run with sudo"
exit 1
fi
# Create directory if needed
mkdir -p "$(dirname "$INSTALL_LOCATION")"
# Download script
echo "Downloading finishte.sh..."
URL="$REPO_URL/$BRANCH_OR_VERSION/$SCRIPT_NAME"
if ! download_file "$URL" "$INSTALL_LOCATION"; then
echo_error "Failed to download from $URL"
exit 1
fi
chmod +x "$INSTALL_LOCATION"
echo_green "✓ Installed to $INSTALL_LOCATION"
echo ""
# Check bash-completion
if [ "$SHELL_TYPE" = "bash" ]; then
if ! command -v _init_completion > /dev/null 2>&1; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
echo_warning "bash-completion not loaded. Add to $RC_FILE:"
echo " source /usr/share/bash-completion/bash_completion"
elif [ -f /etc/bash_completion ]; then
echo_warning "bash-completion not loaded. Add to $RC_FILE:"
echo " source /etc/bash_completion"
else
echo_warning "bash-completion not found. Install it for best experience:"
echo " sudo apt-get install bash-completion # Ubuntu/Debian"
fi
echo ""
fi
fi
# Run finishte install
echo "Running finishte installation..."
if "$INSTALL_LOCATION" install; then
echo ""
echo_green "=========================================="
echo_green " Installation Complete!"
echo_green "=========================================="
echo ""
echo "Next steps:"
echo " 1. Reload your shell configuration:"
echo " source $RC_FILE"
echo ""
echo " 2. Select a language model:"
echo " finishte model"
echo ""
echo " 3. Start using by pressing Tab twice after any command"
echo ""
echo "Documentation: https://git.appmodel.nl/Tour/finish/finish"
else
echo_error "Installation failed. Please check the output above."
exit 1
fi
}
main "$@"

960
finish.sh Normal file
View File

@@ -0,0 +1,960 @@
#!/bin/bash
###############################################################################
# Enhanced Error Handling #
###############################################################################
error_exit() {
echo -e "\e[finish.sh - $1\e[0m" >&2
# In a completion context, exit is too severe. Use return instead.
return 1
}
echo_error() {
echo -e "\e[finish.sh - $1\e[0m" >&2
}
echo_green() {
echo -e "\e[32m$1\e[0m"
}
###############################################################################
# Global Variables & Model Definitions #
###############################################################################
export ACSH_VERSION=0.5.0
unset _finishte_modellist
declare -A _finishte_modellist
# LM-Studio models
_finishte_modellist['lmstudio: darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2']='{ "completion_cost":0.0000000, "prompt_cost":0.0000000, "endpoint": "http://localhost:1234/v1/chat/completions", "model": "darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2", "provider": "lmstudio" }'
# Ollama model
_finishte_modellist['ollama: codellama']='{ "completion_cost":0.0000000, "prompt_cost":0.0000000, "endpoint": "http://localhost:11434/api/chat", "model": "codellama", "provider": "ollama" }'
###############################################################################
# System Information Functions #
###############################################################################
_get_terminal_info() {
local terminal_info=" * User name: \$USER=$USER
* Current directory: \$PWD=$PWD
* Previous directory: \$OLDPWD=$OLDPWD
* Home directory: \$HOME=$HOME
* Operating system: \$OSTYPE=$OSTYPE
* Shell: \$BASH=$BASH
* Terminal type: \$TERM=$TERM
* Hostname: \$HOSTNAME"
echo "$terminal_info"
}
machine_signature() {
local signature
signature=$(echo "$(uname -a)|$$USER" | md5sum | cut -d ' ' -f 1)
echo "$signature"
}
_system_info() {
echo "# System Information"
echo
uname -a
echo "SIGNATURE: $(machine_signature)"
echo
echo "BASH_VERSION: $BASH_VERSION"
echo "BASH_COMPLETION_VERSINFO: ${BASH_COMPLETION_VERSINFO}"
echo
echo "## Terminal Information"
_get_terminal_info
}
_completion_vars() {
echo "BASH_COMPLETION_VERSINFO: ${BASH_COMPLETION_VERSINFO}"
echo "COMP_CWORD: ${COMP_CWORD}"
echo "COMP_KEY: ${COMP_KEY}"
echo "COMP_LINE: ${COMP_LINE}"
echo "COMP_POINT: ${COMP_POINT}"
echo "COMP_TYPE: ${COMP_TYPE}"
echo "COMP_WORDBREAKS: ${COMP_WORDBREAKS}"
echo "COMP_WORDS: ${COMP_WORDS[*]}"
}
###############################################################################
# LLM Completion Functions #
###############################################################################
_get_system_message_prompt() {
echo "You are a helpful bash_completion script. Generate relevant and concise auto-complete suggestions for the given user command in the context of the current directory, operating system, command history, and environment variables. The output must be a list of two to five possible completions or rewritten commands, each on a new line, without spanning multiple lines. Each must be a valid command or chain of commands. Do not include backticks or quotes."
}
_get_output_instructions() {
echo "Provide a list of suggested completions or commands that could be run in the terminal. YOU MUST provide a list of two to five possible completions or rewritten commands. DO NOT wrap the commands in backticks or quotes. Each must be a valid command or chain of commands. Focus on the user's intent, recent commands, and the current environment. RETURN A JSON OBJECT WITH THE COMPLETIONS."
}
_get_command_history() {
local HISTORY_LIMIT=${ACSH_MAX_HISTORY_COMMANDS:-20}
history | tail -n "$HISTORY_LIMIT"
}
# Refined sanitization: only replace long hex sequences, UUIDs, and API-keylike tokens.
_get_clean_command_history() {
local recent_history
recent_history=$(_get_command_history)
recent_history=$(echo "$recent_history" | sed -E 's/\b[[:xdigit:]]{32,40}\b/REDACTED_HASH/g')
recent_history=$(echo "$recent_history" | sed -E 's/\b[0-9a-fA-F-]{36}\b/REDACTED_UUID/g')
recent_history=$(echo "$recent_history" | sed -E 's/\b[A-Za-z0-9]{16,40}\b/REDACTED_APIKEY/g')
echo "$recent_history"
}
_get_recent_files() {
local FILE_LIMIT=${ACSH_MAX_RECENT_FILES:-20}
find . -maxdepth 1 -type f -exec ls -ld {} + | sort -r | head -n "$FILE_LIMIT"
}
# Rewritten _get_help_message using a heredoc to preserve formatting.
_get_help_message() {
local COMMAND HELP_INFO
COMMAND=$(echo "$1" | awk '{print $1}')
HELP_INFO=""
{
set +e
HELP_INFO=$(cat <<EOF
$($COMMAND --help 2>&1 || true)
EOF
)
set -e
} || HELP_INFO="'$COMMAND --help' not available"
echo "$HELP_INFO"
}
_build_prompt() {
local user_input command_history terminal_context help_message recent_files output_instructions other_environment_variables prompt
user_input="$*"
command_history=$(_get_clean_command_history)
terminal_context=$(_get_terminal_info)
help_message=$(_get_help_message "$user_input")
recent_files=$(_get_recent_files)
output_instructions=$(_get_output_instructions)
other_environment_variables=$(env | grep '=' | grep -v 'ACSH_' | awk -F= '{print $1}' | grep -v 'PWD\|OSTYPE\|BASH\|USER\|HOME\|TERM\|OLDPWD\|HOSTNAME')
prompt="User command: \`$user_input\`
# Terminal Context
## Environment variables
$terminal_context
Other defined environment variables
\`\`\`
$other_environment_variables
\`\`\`
## History
Recently run commands (some information redacted):
\`\`\`
$command_history
\`\`\`
## File system
Most recently modified files:
\`\`\`
$recent_files
\`\`\`
## Help Information
$help_message
# Instructions
$output_instructions
"
echo "$prompt"
}
###############################################################################
# Payload Building Functions #
###############################################################################
build_common_payload() {
jq -n --arg model "$model" \
--arg temperature "$temperature" \
--arg system_prompt "$system_prompt" \
--arg prompt_content "$prompt_content" \
'{
model: $model,
messages: [
{role: "system", content: $system_prompt},
{role: "user", content: $prompt_content}
],
temperature: ($temperature | tonumber)
}'
}
_build_payload() {
local user_input prompt system_message_prompt payload acsh_prompt
local model temperature
model="${ACSH_MODEL:-gpt-4o}"
temperature="${ACSH_TEMPERATURE:-0.0}"
user_input="$1"
prompt=$(_build_prompt "$@")
system_message_prompt=$(_get_system_message_prompt)
acsh_prompt="# SYSTEM PROMPT
$system_message_prompt
# USER MESSAGE
$prompt"
export ACSH_PROMPT="$acsh_prompt"
prompt_content="$prompt"
system_prompt="$system_message_prompt"
local base_payload
base_payload=$(build_common_payload)
case "${ACSH_PROVIDER^^}" in
"OLLAMA")
payload=$(echo "$base_payload" | jq '. + {
format: "json",
stream: false,
options: {temperature: (.temperature | tonumber)}
}')
;;
"LMSTUDIO")
payload=$(echo "$base_payload" | jq '. + {response_format: {type: "json_object"}}')
;;
*)
payload=$(echo "$base_payload" | jq '. + {response_format: {type: "json_object"}}')
;;
esac
echo "$payload"
}
log_request() {
local user_input response_body user_input_hash log_file prompt_tokens completion_tokens created api_cost
local prompt_tokens_int completion_tokens_int
user_input="$1"
response_body="$2"
user_input_hash=$(echo -n "$user_input" | md5sum | cut -d ' ' -f 1)
prompt_tokens=$(echo "$response_body" | jq -r '.usage.prompt_tokens')
prompt_tokens_int=$((prompt_tokens))
completion_tokens=$(echo "$response_body" | jq -r '.usage.completion_tokens')
completion_tokens_int=$((completion_tokens))
created=$(date +%s)
created=$(echo "$response_body" | jq -r ".created // $created")
api_cost=$(echo "$prompt_tokens_int * $ACSH_API_PROMPT_COST + $completion_tokens_int * $ACSH_API_COMPLETION_COST" | bc)
log_file=${ACSH_LOG_FILE:-"$HOME/.finish/finish.log"}
echo "$created,$user_input_hash,$prompt_tokens_int,$completion_tokens_int,$api_cost" >> "$log_file"
}
openai_completion() {
local content status_code response_body default_user_input user_input api_key payload endpoint timeout attempt max_attempts
endpoint=${ACSH_ENDPOINT:-"http://localhost:1234/v1/chat/completions"}
timeout=${ACSH_TIMEOUT:-30}
default_user_input="Write two to six most likely commands given the provided information"
user_input=${*:-$default_user_input}
if [[ -z "$ACSH_ACTIVE_API_KEY" && ${ACSH_PROVIDER^^} != "OLLAMA" && ${ACSH_PROVIDER^^} != "LMSTUDIO" ]]; then
echo_error "ACSH_ACTIVE_API_KEY not set. Please set it with: export ${ACSH_PROVIDER^^}_API_KEY=<your-api-key>"
return
fi
api_key="${ACSH_ACTIVE_API_KEY}"
payload=$(_build_payload "$user_input")
max_attempts=2
attempt=1
while [ $attempt -le $max_attempts ]; do
if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then
response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" --data "$payload")
else
response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \
-H "Content-Type: application/json" \
-d "$payload")
fi
status_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | sed '$d')
if [[ $status_code -eq 200 ]]; then
break
else
echo_error "API call failed with status $status_code. Retrying... (Attempt $attempt of $max_attempts)"
sleep 1
attempt=$((attempt+1))
fi
done
if [[ $status_code -ne 200 ]]; then
case $status_code in
400) echo_error "Bad Request: The API request was invalid or malformed." ;;
401) echo_error "Unauthorized: The provided API key is invalid or missing." ;;
429) echo_error "Too Many Requests: The API rate limit has been exceeded." ;;
500) echo_error "Internal Server Error: An unexpected error occurred on the API server." ;;
*) echo_error "Unknown Error: Unexpected status code $status_code received. Response: $response_body" ;;
esac
return
fi
if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then
content=$(echo "$response_body" | jq -r '.message.content')
content=$(echo "$content" | jq -r '.completions')
else
content=$(echo "$response_body" | jq -r '.choices[0].message.content')
content=$(echo "$content" | jq -r '.completions')
fi
local completions
completions=$(echo "$content" | jq -r '.[]' | grep -v '^$')
echo -n "$completions"
log_request "$user_input" "$response_body"
}
###############################################################################
# Completion Functions #
###############################################################################
_get_default_completion_function() {
local cmd="$1"
complete -p "$cmd" 2>/dev/null | awk -F' ' '{ for(i=1;i<=NF;i++) { if ($i ~ /^-F$/) { print $(i+1); exit; } } }'
}
_default_completion() {
local current_word="" first_word="" default_func
if [[ -n "${COMP_WORDS[*]}" ]]; then
first_word="${COMP_WORDS[0]}"
if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then
current_word="${COMP_WORDS[COMP_CWORD]}"
fi
fi
default_func=$(_get_default_completion_function "$first_word")
if [[ -n "$default_func" ]]; then
"$default_func"
else
local file_completions
if [[ -z "$current_word" ]]; then
file_completions=$(compgen -f -- || true)
else
file_completions=$(compgen -f -- "$current_word" || true)
fi
if [[ -n "$file_completions" ]]; then
readarray -t COMPREPLY <<<"$file_completions"
fi
fi
}
list_cache() {
local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"}
find "$cache_dir" -maxdepth 1 -type f -name "acsh-*" -printf '%T+ %p\n' | sort
}
_finishtesh() {
_init_completion || return
_default_completion
if [[ ${#COMPREPLY[@]} -eq 0 && $COMP_TYPE -eq 63 ]]; then
local completions user_input user_input_hash
acsh_load_config
if [[ -z "$ACSH_ACTIVE_API_KEY" && ${ACSH_PROVIDER^^} != "OLLAMA" && ${ACSH_PROVIDER^^} != "LMSTUDIO" ]]; then
local provider_key="${ACSH_PROVIDER}_API_KEY"
provider_key=$(echo "$provider_key" | tr '[:lower:]' '[:upper:]')
echo_error "${provider_key} is not set. Please set it using: export ${provider_key}=<your-api-key> or disable finish via: finish disable"
echo
return
fi
if [[ -n "${COMP_WORDS[*]}" ]]; then
command="${COMP_WORDS[0]}"
if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then
current="${COMP_WORDS[COMP_CWORD]}"
fi
fi
user_input="${COMP_LINE:-"$command $current"}"
user_input_hash=$(echo -n "$user_input" | md5sum | cut -d ' ' -f 1)
export ACSH_INPUT="$user_input"
export ACSH_PROMPT=
export ACSH_RESPONSE=
local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"}
local cache_size=${ACSH_CACHE_SIZE:-100}
local cache_file="$cache_dir/acsh-$user_input_hash.txt"
if [[ -d "$cache_dir" && "$cache_size" -gt 0 && -f "$cache_file" ]]; then
completions=$(cat "$cache_file" || true)
touch "$cache_file"
else
echo -en "\e]12;green\a"
completions=$(openai_completion "$user_input" || true)
if [[ -z "$completions" ]]; then
echo -en "\e]12;red\a"
sleep 1
completions=$(openai_completion "$user_input" || true)
fi
echo -en "\e]12;white\a"
if [[ -d "$cache_dir" && "$cache_size" -gt 0 ]]; then
echo "$completions" > "$cache_file"
while [[ $(list_cache | wc -l) -gt "$cache_size" ]]; do
oldest=$(list_cache | head -n 1 | cut -d ' ' -f 2-)
rm "$oldest" || true
done
fi
fi
export ACSH_RESPONSE=$completions
if [[ -n "$completions" ]]; then
local num_rows
num_rows=$(echo "$completions" | wc -l)
COMPREPLY=()
if [[ $num_rows -eq 1 ]]; then
readarray -t COMPREPLY <<<"$(echo -n "${completions}" | sed "s/${command}[[:space:]]*//" | sed 's/:/\\:/g')"
else
completions=$(echo "$completions" | awk '{print NR". "$0}')
readarray -t COMPREPLY <<< "$completions"
fi
fi
if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
COMPREPLY=("$current")
fi
fi
}
###############################################################################
# CLI Commands & Configuration Management #
###############################################################################
show_help() {
echo_green "finish.sh - LLM Powered Bash Completion"
echo "Usage: finish [options] command"
echo " finish [options] install|remove|config|model|enable|disable|clear|usage|system|command|--help"
echo
echo "finish.sh enhances bash completion with LLM capabilities."
echo "Press Tab twice for suggestions."
echo "Commands:"
echo " command Run finish (simulate double Tab)"
echo " command --dry-run Show prompt without executing"
echo " model Change language model"
echo " usage Display usage stats"
echo " system Display system information"
echo " config Show or set configuration values"
echo " config set <key> <value> Set a config value"
echo " config reset Reset config to defaults"
echo " install Install finish to .bashrc"
echo " remove Remove installation from .bashrc"
echo " enable Enable finish"
echo " disable Disable finish"
echo " clear Clear cache and log files"
echo " --help Show this help message"
echo
echo "Submit issues at: https://git.appmodel.nl/Tour/finish/issues"
}
is_subshell() {
if [[ "$$" != "$BASHPID" ]]; then
return 0
else
return 1
fi
}
show_config() {
local config_file="$HOME/.finish/config" term_width small_table
echo_green "finish.sh - Configuration and Settings - Version $ACSH_VERSION"
if is_subshell; then
echo " STATUS: Unknown. Run 'source finish config' to check status."
return
elif check_if_enabled; then
echo -e " STATUS: \033[32;5mEnabled\033[0m"
else
echo -e " STATUS: \033[31;5mDisabled\033[0m - Run 'source finish config' to verify."
fi
if [ ! -f "$config_file" ]; then
echo_error "Configuration file not found: $config_file. Run finish install."
return
fi
acsh_load_config
term_width=$(tput cols)
if [[ $term_width -gt 70 ]]; then
term_width=70; small_table=0
fi
if [[ $term_width -lt 40 ]]; then
term_width=70; small_table=1
fi
for config_var in $(compgen -v | grep ACSH_); do
if [[ $config_var == "ACSH_INPUT" || $config_var == "ACSH_PROMPT" || $config_var == "ACSH_RESPONSE" ]]; then
continue
fi
config_value="${!config_var}"
if [[ ${config_var: -8} == "_API_KEY" ]]; then
continue
fi
echo -en " $config_var:\e[90m"
if [[ $small_table -eq 1 ]]; then
echo -e "\n $config_value\e[0m"
else
printf '%s%*s' "" $((term_width - ${#config_var} - ${#config_value} - 3)) ''
echo -e "$config_value\e[0m"
fi
done
echo -e " ===================================================================="
for config_var in $(compgen -v | grep ACSH_); do
if [[ $config_var == "ACSH_INPUT" || $config_var == "ACSH_PROMPT" || $config_var == "ACSH_RESPONSE" ]]; then
continue
fi
if [[ ${config_var: -8} != "_API_KEY" ]]; then
continue
fi
echo -en " $config_var:\e[90m"
if [[ -z ${!config_var} ]]; then
config_value="UNSET"
echo -en "\e[31m"
else
rest=${!config_var:4}
config_value="${!config_var:0:4}...${rest: -4}"
echo -en "\e[32m"
fi
if [[ $small_table -eq 1 ]]; then
echo -e "\n $config_value\e[0m"
else
printf '%s%*s' "" $((term_width - ${#config_var} - ${#config_value} - 3)) ''
echo -e "$config_value\e[0m"
fi
done
}
set_config() {
local key="$1" value="$2" config_file="$HOME/.finish/config"
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
key=$(echo "$key" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')
if [ -z "$key" ]; then
echo_error "SyntaxError: expected 'finish config set <key> <value>'"
return
fi
if [ ! -f "$config_file" ]; then
echo_error "Configuration file not found: $config_file. Run finishte install."
return
fi
sed -i "s|^\($key:\).*|\1 $value|" "$config_file"
acsh_load_config
}
config_command() {
local command config_file="$HOME/.finishte/config"
command="${*:2}"
if [ -z "$command" ]; then
show_config
return
fi
if [ "$2" == "set" ]; then
local key="$3" value="$4"
echo "Setting configuration key '$key' to '$value'"
set_config "$key" "$value"
echo_green "Configuration updated. Run 'finishte config' to view changes."
return
fi
if [[ "$command" == "reset" ]]; then
echo "Resetting configuration to default values."
rm "$config_file" || true
build_config
return
fi
echo_error "SyntaxError: expected 'finishte config set <key> <value>' or 'finishte config reset'"
}
build_config() {
local config_file="$HOME/.finishte/config" default_config
if [ ! -f "$config_file" ]; then
echo "Creating default configuration file at ~/.finishte/config"
default_config="# ~/.finishte/config
# Model configuration
provider: lmstudio
model: darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2
temperature: 0.0
endpoint: http://localhost:1234/v1/chat/completions
api_prompt_cost: 0.000000
api_completion_cost: 0.000000
# Max history and recent files
max_history_commands: 20
max_recent_files: 20
# Cache settings
cache_dir: $HOME/.finishte/cache
cache_size: 10
# Logging settings
log_file: $HOME/.finishte/finishte.log"
echo "$default_config" > "$config_file"
fi
}
acsh_load_config() {
local config_file="$HOME/.finishte/config" key value
if [ -f "$config_file" ]; then
while IFS=':' read -r key value; do
if [[ $key =~ ^# ]] || [[ -z $key ]]; then
continue
fi
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
key=$(echo "$key" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')
if [[ -n $value ]]; then
export "ACSH_$key"="$value"
fi
done < "$config_file"
if [[ -z "$ACSH_OLLAMA_API_KEY" && -n "$LLM_API_KEY" ]]; then
export ACSH_OLLAMA_API_KEY="$LLM_API_KEY"
fi
# If the custom API key was set, map it to OLLAMA if needed.
if [[ -z "$ACSH_OLLAMA_API_KEY" && -n "$ACSH_CUSTOM_API_KEY" ]]; then
export ACSH_OLLAMA_API_KEY="$ACSH_CUSTOM_API_KEY"
fi
case "${ACSH_PROVIDER:-lmstudio}" in
"ollama") export ACSH_ACTIVE_API_KEY="$ACSH_OLLAMA_API_KEY" ;;
"lmstudio") export ACSH_ACTIVE_API_KEY="" ;;
*) echo_error "Unknown provider: $ACSH_PROVIDER" ;;
esac
else
echo "Configuration file not found: $config_file"
fi
}
install_command() {
local bashrc_file="$HOME/.bashrc" finishte_setup="source finishte enable" finishte_cli_setup="complete -F _finishtesh_cli finishte"
if ! command -v finishte &>/dev/null; then
echo_error "finishte.sh not in PATH. Follow install instructions at https://git.appmodel.nl/Tour/finish"
return
fi
if [[ ! -d "$HOME/.finishte" ]]; then
echo "Creating ~/.finishte directory"
mkdir -p "$HOME/.finishte"
fi
local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"}
if [[ ! -d "$cache_dir" ]]; then
mkdir -p "$cache_dir"
fi
build_config
acsh_load_config
if ! grep -qF "$finishte_setup" "$bashrc_file"; then
echo -e "# finishte.sh" >> "$bashrc_file"
echo -e "$finishte_setup\n" >> "$bashrc_file"
echo "Added finishte.sh setup to $bashrc_file"
else
echo "finishte.sh setup already exists in $bashrc_file"
fi
if ! grep -qF "$finishte_cli_setup" "$bashrc_file"; then
echo -e "# finishte.sh CLI" >> "$bashrc_file"
echo -e "$finishte_cli_setup\n" >> "$bashrc_file"
echo "Added finishte CLI completion to $bashrc_file"
fi
echo
echo_green "finishte.sh - Version $ACSH_VERSION installation complete."
echo -e "Run: source $bashrc_file to enable finishte."
echo -e "Then run: finishte model to select a language model."
}
remove_command() {
local config_file="$HOME/.finishte/config" cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"} log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"} bashrc_file="$HOME/.bashrc"
echo_green "Removing finishte.sh installation..."
[ -f "$config_file" ] && { rm "$config_file"; echo "Removed: $config_file"; }
[ -d "$cache_dir" ] && { rm -rf "$cache_dir"; echo "Removed: $cache_dir"; }
[ -f "$log_file" ] && { rm "$log_file"; echo "Removed: $log_file"; }
if [ -d "$HOME/.finishte" ]; then
if [ -z "$(ls -A "$HOME/.finishte")" ]; then
rmdir "$HOME/.finishte"
echo "Removed: $HOME/.finishte"
else
echo "Skipped removing $HOME/.finishte (not empty)"
fi
fi
if [ -f "$bashrc_file" ]; then
if grep -qF "source finishte enable" "$bashrc_file"; then
sed -i '/# finishte.sh/d' "$bashrc_file"
sed -i '/finishte/d' "$bashrc_file"
echo "Removed finishte.sh setup from $bashrc_file"
fi
fi
local finishte_script
finishte_script=$(command -v finishte)
if [ -n "$finishte_script" ]; then
echo "finishte script is at: $finishte_script"
if [ "$1" == "-y" ]; then
rm "$finishte_script"
echo "Removed: $finishte_script"
else
read -r -p "Remove the finishte script? (y/n): " confirm
if [[ $confirm == "y" ]]; then
rm "$finishte_script"
echo "Removed: $finishte_script"
fi
fi
fi
echo "Uninstallation complete."
}
check_if_enabled() {
local is_enabled
is_enabled=$(complete -p | grep _finishtesh | grep -cv _finishtesh_cli)
(( is_enabled > 0 )) && return 0 || return 1
}
_finishtesh_cli() {
if [[ -n "${COMP_WORDS[*]}" ]]; then
command="${COMP_WORDS[0]}"
if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then
current="${COMP_WORDS[COMP_CWORD]}"
fi
fi
if [[ $current == "config" ]]; then
readarray -t COMPREPLY <<< "set
reset"
return
elif [[ $current == "command" ]]; then
readarray -t COMPREPLY <<< "command --dry-run"
return
fi
if [[ -z "$current" ]]; then
readarray -t COMPREPLY <<< "install
remove
config
enable
disable
clear
usage
system
command
model
--help"
fi
}
enable_command() {
if check_if_enabled; then
echo_green "Reloading finishte.sh..."
disable_command
fi
acsh_load_config
complete -D -E -F _finishtesh -o nospace
}
disable_command() {
if check_if_enabled; then
complete -F _completion_loader -D
fi
}
command_command() {
local args=("$@")
for ((i = 0; i < ${#args[@]}; i++)); do
if [ "${args[i]}" == "--dry-run" ]; then
args[i]=""
_build_prompt "${args[@]}"
return
fi
done
openai_completion "$@" || true
echo
}
clear_command() {
local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"} log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"}
echo "This will clear the cache and log file."
echo -e "Cache directory: \e[31m$cache_dir\e[0m"
echo -e "Log file: \e[31m$log_file\e[0m"
read -r -p "Are you sure? (y/n): " confirm
if [[ $confirm != "y" ]]; then
echo "Aborted."
return
fi
if [ -d "$cache_dir" ]; then
local cache_files
cache_files=$(list_cache)
if [ -n "$cache_files" ]; then
while read -r line; do
file=$(echo "$line" | cut -d ' ' -f 2-)
rm "$file"
echo "Removed: $file"
done <<< "$cache_files"
echo "Cleared cache in: $cache_dir"
else
echo "Cache is empty."
fi
fi
[ -f "$log_file" ] && { rm "$log_file"; echo "Removed: $log_file"; }
}
usage_command() {
local log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"} cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"}
local cache_size number_of_lines api_cost avg_api_cost
cache_size=$(list_cache | wc -l)
echo_green "finishte.sh - Usage Information"
echo
echo -n "Log file: "; echo -e "\e[90m$log_file\e[0m"
if [ ! -f "$log_file" ]; then
number_of_lines=0
api_cost=0
avg_api_cost=0
else
number_of_lines=$(wc -l < "$log_file")
api_cost=$(awk -F, '{sum += $5} END {print sum}' "$log_file")
avg_api_cost=$(echo "$api_cost / $number_of_lines" | bc -l)
fi
echo
echo -e "\tUsage count:\t\e[32m$number_of_lines\e[0m"
echo -e "\tAvg Cost:\t\$$(printf "%.4f" "$avg_api_cost")"
echo -e "\tTotal Cost:\t\e[31m\$$(printf "%.4f" "$api_cost")\e[0m"
echo
echo -n "Cache Size: $cache_size of ${ACSH_CACHE_SIZE:-10} in "; echo -e "\e[90m$cache_dir\e[0m"
echo "To clear log and cache, run: finishte clear"
}
###############################################################################
# Enhanced Interactive Menu UX #
###############################################################################
get_key() {
IFS= read -rsn1 key 2>/dev/null >&2
if [[ $key == $'\x1b' ]]; then
read -rsn2 key
if [[ $key == [A ]]; then echo up; fi
if [[ $key == [B ]]; then echo down; fi
if [[ $key == q ]]; then echo q; fi
elif [[ $key == "q" ]]; then
echo q
else
echo "$key"
fi
}
menu_selector() {
options=("$@")
selected=0
show_menu() {
echo
echo "Select a Language Model (Up/Down arrows, Enter to select, 'q' to quit):"
for i in "${!options[@]}"; do
if [[ $i -eq $selected ]]; then
echo -e "\e[1;32m> ${options[i]}\e[0m"
else
echo " ${options[i]}"
fi
done
}
tput sc
while true; do
tput rc; tput ed
show_menu
key=$(get_key)
case $key in
up)
((selected--))
if ((selected < 0)); then
selected=$((${#options[@]} - 1))
fi
;;
down)
((selected++))
if ((selected >= ${#options[@]})); then
selected=0
fi
;;
q)
echo "Selection canceled."
return 1
;;
"")
break
;;
esac
done
clear
return $selected
}
model_command() {
clear
local selected_model options=()
if [[ $# -ne 3 ]]; then
mapfile -t sorted_keys < <(for key in "${!_finishte_modellist[@]}"; do echo "$key"; done | sort)
for key in "${sorted_keys[@]}"; do
options+=("$key")
done
echo -e "\e[1;32mfinishte.sh - Model Configuration\e[0m"
menu_selector "${options[@]}"
selected_option=$?
if [[ $selected_option -eq 1 ]]; then
return
fi
selected_model="${options[selected_option]}"
selected_value="${_finishte_modellist[$selected_model]}"
else
provider="$2"
model_name="$3"
selected_value="${_finishte_modellist["$provider: $model_name"]}"
if [[ -z "$selected_value" ]]; then
echo "ERROR: Invalid provider or model name."
return 1
fi
fi
set_config "model" "$(echo "$selected_value" | jq -r '.model')"
set_config "endpoint" "$(echo "$selected_value" | jq -r '.endpoint')"
set_config "provider" "$(echo "$selected_value" | jq -r '.provider')"
prompt_cost=$(echo "$selected_value" | jq -r '.prompt_cost' | awk '{printf "%.8f", $1}')
completion_cost=$(echo "$selected_value" | jq -r '.completion_cost' | awk '{printf "%.8f", $1}')
set_config "api_prompt_cost" "$prompt_cost"
set_config "api_completion_cost" "$completion_cost"
model="${ACSH_MODEL:-ERROR}"
temperature=$(echo "${ACSH_TEMPERATURE:-0.0}" | awk '{printf "%.3f", $1}')
echo -e "Provider:\t\e[90m$ACSH_PROVIDER\e[0m"
echo -e "Model:\t\t\e[90m$model\e[0m"
echo -e "Temperature:\t\e[90m$temperature\e[0m"
echo
echo -e "Cost/token:\t\e[90mprompt: \$$ACSH_API_PROMPT_COST, completion: \$$ACSH_API_COMPLETION_COST\e[0m"
echo -e "Endpoint:\t\e[90m$ACSH_ENDPOINT\e[0m"
if [[ ${ACSH_PROVIDER^^} == "OLLAMA" || ${ACSH_PROVIDER^^} == "LMSTUDIO" ]]; then
echo "To set a custom endpoint:"
echo -e "\t\e[34mfinishte config set endpoint <your-url>\e[0m"
echo "Other models can be set with:"
echo -e "\t\e[34mfinishte config set model <model-name>\e[0m"
fi
echo "To change temperature:"
echo -e "\t\e[90mfinishte config set temperature <temperature>\e[0m"
echo
}
###############################################################################
# CLI ENTRY POINT #
###############################################################################
case "$1" in
"--help")
show_help
;;
system)
_system_info
;;
install)
install_command
;;
remove)
remove_command "$@"
;;
clear)
clear_command
;;
usage)
usage_command
;;
model)
model_command "$@"
;;
config)
config_command "$@"
;;
enable)
enable_command
;;
disable)
disable_command
;;
command)
command_command "$@"
;;
*)
if [[ -n "$1" ]]; then
echo_error "Unknown command $1 - run 'finishte --help' for usage or visit https://finishte.sh"
else
echo_green "finishte.sh - LLM Powered Bash Completion - Version $ACSH_VERSION - https://finishte.sh"
fi
;;
esac

47
homebrew/finish.rb Normal file
View File

@@ -0,0 +1,47 @@
class finishteSh < Formula
desc "LLM-powered command-line finishtion"
homepage "https://git.appmodel.nl/Tour/finishte"
url "https://git.appmodel.nl/Tour/finish/archive/v0.5.0.tar.gz"
sha256 "" # Update with actual SHA256 of the release tarball
license "BSD-2-Clause"
version "0.5.0"
depends_on "jq"
depends_on "bash" => :optional
depends_on "bash-completion@2" => :optional
def install
bin.install "finishte.sh" => "finishte"
doc.install "README.md"
doc.install "LICENSE"
end
def caveats
<<~EOS
To complete the installation:
1. Run the installer:
finishte install
2. Reload your shell:
source ~/.bashrc # for bash
source ~/.zshrc # for zsh
3. Select a language model:
finishte model
You'll need LM-Studio or Ollama running locally to use finishte.sh.
LM-Studio: https://lmstudio.ai
Ollama: https://ollama.ai
For more information, visit:
https:/git.appmodel.nl/Tour/finishte
EOS
end
test do
system "#{bin}/finishte", "--help"
assert_match "finishte.sh", shell_output("#{bin}/finishte --help")
end
end

3
run_tests.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
bats tests/

43
tests/test_finish.bats Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bats
setup() {
# Install finishte.sh and run testing against the main branch
wget -qO- https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash -s -- main
# Source bashrc to make sure finishte is available in the current session
source ~/.bashrc
# Configure for local LM-Studio
finishte config set provider lmstudio
finishte config set endpoint http://localhost:1234/v1/chat/completions
finishte config set model darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2
}
teardown() {
# Remove finishte.sh installation
finishte remove -y
}
@test "which finishte returns something" {
run which finishte
[ "$status" -eq 0 ]
[ -n "$output" ]
}
@test "finishte returns a string containing finishte.sh (case insensitive)" {
run finishte
[ "$status" -eq 0 ]
[[ "$output" =~ [Aa]utocomplete\.sh ]]
}
@test "finishte config should have lmstudio provider" {
run finishte config
[ "$status" -eq 0 ]
[[ "$output" =~ lmstudio ]]
}
@test "finishte command 'ls # show largest files' should return something" {
run finishte command "ls # show largest files"
[ "$status" -eq 0 ]
[ -n "$output" ]
}