#
tokens: 5355/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       └── release.yml
├── .gitignore
├── .gitmessage
├── .python-version
├── CHANGELOG.md
├── ollamatools.py
├── pyproject.toml
├── README.md
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.14
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python-generated files
 2 | __pycache__/
 3 | *.py[oc]
 4 | build/
 5 | dist/
 6 | wheels/
 7 | *.egg-info
 8 | 
 9 | # Virtual environments
10 | .venv
11 | .ruff_cache/
12 | .python-version
```

--------------------------------------------------------------------------------
/.gitmessage:
--------------------------------------------------------------------------------

```
 1 | # <type>(<scope>): <subject>
 2 | #
 3 | # <body>
 4 | #
 5 | # <footer>
 6 | 
 7 | # Type should be one of the following:
 8 | # * feat: A new feature
 9 | # * fix: A bug fix
10 | # * docs: Documentation only changes
11 | # * style: Changes that do not affect the meaning of the code
12 | # * refactor: A code change that neither fixes a bug nor adds a feature
13 | # * perf: A code change that improves performance
14 | # * test: Adding missing tests or correcting existing tests
15 | # * build: Changes that affect the build system or external dependencies
16 | # * ci: Changes to our CI configuration files and scripts
17 | # * chore: Other changes that don't modify src or test files
18 | # * revert: Reverts a previous commit
19 | 
20 | # Scope is optional and should be the name of the package affected
21 | # (as perceived by the person reading the changelog)
22 | 
23 | # Subject line should:
24 | # * use the imperative, present tense: "change" not "changed" nor "changes"
25 | # * not capitalize the first letter
26 | # * not end with a dot (.)
27 | 
28 | # Body should include the motivation for the change and contrast this with previous behavior
29 | 
30 | # Footer should contain:
31 | # * Information about Breaking Changes
32 | # * Reference GitHub issues that this commit closes
33 | 
34 | # Examples:
35 | # feat(parser): add ability to parse arrays
36 | # fix(release): need to depend on latest rxjs and zone.js
37 | # docs(changelog): update changelog to beta.5
38 | # fix(release): need to depend on latest rxjs and zone.js
39 | # feat(lang): add polish language
40 | # perf(core): improve bundle size by removing debug code
41 | # BREAKING CHANGE: The graphiteWidth option has been removed.
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Ollama Tool CLI 🦙
  2 | 
  3 | A modern CLI tool for managing Ollama models - backup, restore, update, and list your models with ease.
  4 | 
  5 | ## Installation
  6 | 
  7 | ### Using pip (recommended)
  8 | 
  9 | ```bash
 10 | pip install ollama-tool-cli
 11 | ```
 12 | 
 13 | ### Using uv
 14 | 
 15 | ```bash
 16 | uv add ollama-tool-cli
 17 | ```
 18 | 
 19 | ### From source
 20 | 
 21 | ```bash
 22 | git clone https://github.com/arian24b/ollamatools.git
 23 | cd ollamatools
 24 | uv sync
 25 | ```
 26 | 
 27 | ## Requirements
 28 | 
 29 | - Python 3.10 or higher
 30 | - Ollama installed and running
 31 | 
 32 | ## Usage
 33 | 
 34 | ### Basic Commands
 35 | 
 36 | ```bash
 37 | # Show help
 38 | ollama-tool-cli
 39 | 
 40 | # List all installed models
 41 | ollama-tool-cli list
 42 | 
 43 | # Update all models
 44 | ollama-tool-cli update
 45 | 
 46 | # Update a specific model
 47 | ollama-tool-cli update llama3.2
 48 | 
 49 | # Backup all models to default location (~/Downloads/ollama_model_backups)
 50 | ollama-tool-cli backup
 51 | 
 52 | # Backup to custom path
 53 | ollama-tool-cli backup --path /path/to/backup
 54 | 
 55 | # Backup a specific model
 56 | ollama-tool-cli backup --model llama3.2
 57 | 
 58 | # Restore from backup
 59 | ollama-tool-cli restore /path/to/backup.zip
 60 | 
 61 | # Show Ollama version
 62 | ollama-tool-cli version
 63 | 
 64 | # Show installation information
 65 | ollama-tool-cli info
 66 | 
 67 | # Check if Ollama is installed
 68 | ollama-tool-cli check
 69 | ```
 70 | 
 71 | ### Command Details
 72 | 
 73 | #### `list`
 74 | Display all installed Ollama models with their versions.
 75 | 
 76 | #### `update [model]`
 77 | Update one or all Ollama models. If no model name is provided, updates all models.
 78 | 
 79 | #### `backup [--path PATH] [--model MODEL]`
 80 | Backup Ollama models to zip files. By default backs up all models to `~/Downloads/ollama_model_backups`.
 81 | 
 82 | - `--path, -p`: Custom backup directory path
 83 | - `--model, -m`: Backup only a specific model
 84 | 
 85 | #### `restore <path>`
 86 | Restore Ollama models from a backup zip file or directory.
 87 | 
 88 | #### `version`
 89 | Display the installed Ollama version.
 90 | 
 91 | #### `info`
 92 | Show detailed Ollama installation information including version, models path, platform, and number of installed models.
 93 | 
 94 | #### `check`
 95 | Verify that Ollama is installed and accessible.
 96 | 
 97 | ## Development
 98 | 
 99 | ### Setup development environment
100 | 
101 | ```bash
102 | uv sync
103 | ```
104 | 
105 | ### Build the package
106 | 
107 | ```bash
108 | uv build
109 | ```
110 | 
111 | ## License
112 | 
113 | MIT License - see LICENSE file for details.
114 | 
115 | ## Contributing
116 | 
117 | Contributions are welcome! Please feel free to submit a Pull Request.
118 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # CHANGELOG
 2 | 
 3 | <!-- version list -->
 4 | 
 5 | ## v1.1.1 (2025-12-27)
 6 | 
 7 | ### Bug Fixes
 8 | 
 9 | - Update Git configuration to use GitHub actor and modify URLs in pyproject.toml
10 |   ([`ebc97c2`](https://github.com/arian24b/ollamatools/commit/ebc97c2d46ad90ea1e5f2eec3da25682a60d25f6))
11 | 
12 | 
13 | ## v1.1.0 (2025-12-27)
14 | 
15 | ### Bug Fixes
16 | 
17 | - Change build command to uv build for semantic-release compatibility
18 |   ([`0ec8798`](https://github.com/arian24b/ollamatools/commit/0ec87980bd70119ec2a775d9427f15fc4a19832f))
19 | 
20 | ### Chores
21 | 
22 | - Update version to 1.0.1 and modify build settings
23 |   ([`e55f3ff`](https://github.com/arian24b/ollamatools/commit/e55f3ffdd2303d8b8c55b6086b1e78fb380e9964))
24 | 
25 | ### Features
26 | 
27 | - Rename package to ollama-tool-cli and CLI command to ollama-tool-cli
28 |   ([`aa2843f`](https://github.com/arian24b/ollamatools/commit/aa2843fd341753263cd5600051b3108d00dceed7))
29 | 
30 | 
31 | ## v1.0.0 (2025-12-27)
32 | 
33 | - Initial Release
34 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Release
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 |   workflow_dispatch:
 8 | 
 9 | permissions:
10 |   contents: write
11 |   id-token: write
12 | 
13 | jobs:
14 |   release:
15 |     runs-on: ubuntu-latest
16 | 
17 |     steps:
18 |       - name: Checkout repository
19 |         uses: actions/checkout@v4
20 |         with:
21 |           fetch-depth: 0
22 | 
23 |       - name: Set up UV
24 |         uses: astral-sh/setup-uv@v7
25 |         with:
26 |           python-version: '3.14'
27 | 
28 |       - name: Install dependencies
29 |         run: |
30 |           uv tool install python-semantic-release
31 | 
32 |       - name: Configure Git
33 |         run: |
34 |           git config user.name ${{ github.actor }}
35 |           git config user.email ${{ github.actor }}@users.noreply.github.com
36 | 
37 |       - name: Semantic Release (Version, Tag, GitHub Release)
38 |         id: semantic-release
39 |         env:
40 |           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 |         run: |
42 |           semantic-release version --changelog --tag
43 | 
44 |       - name: Build Package
45 |         if: steps.semantic-release.outputs.version != ''
46 |         run: |
47 |           uv sync
48 |           uv build
49 | 
50 |       - name: Publish to PyPI
51 |         if: steps.semantic-release.outputs.version != ''
52 |         uses: pypa/gh-action-pypi-publish@release/v1
53 |         with:
54 |           password: ${{ secrets.PYPI_API_TOKEN }}
55 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "ollama-tool-cli"
 3 | version = "1.1.1"
 4 | description = "CLI tool for managing Ollama models - backup, restore, update, and list models"
 5 | readme = "README.md"
 6 | requires-python = ">=3.10"
 7 | dependencies = [
 8 |     "typer>=0.12.0",
 9 | ]
10 | authors = [
11 |     {name = "Arian Omrani", email = "[email protected]"}
12 | ]
13 | license = "MIT"
14 | classifiers = [
15 |     "Development Status :: 3 - Alpha",
16 |     "Intended Audience :: Developers",
17 |     "Operating System :: OS Independent",
18 |     "Programming Language :: Python :: 3",
19 |     "Programming Language :: Python :: 3.10",
20 |     "Programming Language :: Python :: 3.11",
21 |     "Programming Language :: Python :: 3.12",
22 |     "Programming Language :: Python :: 3.13",
23 |     "Programming Language :: Python :: 3.14",
24 | ]
25 | keywords = ["ollama", "cli", "models", "backup", "restore", "llm"]
26 | 
27 | [project.optional-dependencies]
28 | dev = ["ruff", "pytest", "build"]
29 | 
30 | [project.scripts]
31 | ollama-tool-cli = "ollamatools:app"
32 | 
33 | [project.urls]
34 | Homepage = "https://github.com/arian24b/ollamatools"
35 | Repository = "https://github.com/arian24b/ollamatools"
36 | Issues = "https://github.com/arian24b/ollamatools/issues"
37 | 
38 | [build-system]
39 | requires = ["setuptools>=61.0", "wheel"]
40 | build-backend = "setuptools.build_meta"
41 | 
42 | [tool.semantic_release]
43 | commit_parser = "conventional"
44 | commit_author = "Arian Omrani <[email protected]>"
45 | branch = "main"
46 | version_toml = ["pyproject.toml:project.version"]
47 | upload_to_vcs_release = false
48 | build_command = "uv build"
49 | 
```

--------------------------------------------------------------------------------
/ollamatools.py:
--------------------------------------------------------------------------------

```python
  1 | from dataclasses import dataclass
  2 | from json import loads
  3 | from pathlib import Path
  4 | from subprocess import PIPE, Popen
  5 | from sys import platform
  6 | from zipfile import ZipFile
  7 | 
  8 | import typer
  9 | 
 10 | # Test commit for patch release
 11 | 
 12 | 
 13 | @dataclass
 14 | class CMDOutput:
 15 |     output_text: str
 16 |     error_text: str
 17 |     return_code: int
 18 | 
 19 |     def __str__(self) -> str:
 20 |         return f"Output Text: {self.output_text}\nError Text: {self.error_text}\nReturn Code: {self.return_code}"
 21 | 
 22 | 
 23 | MODELS_PATH = {
 24 |     "linux": Path("/usr/share/ollama/.ollama/models").expanduser(),
 25 |     "macos": Path("~/.ollama/models").expanduser(),
 26 |     "windows": Path("C:\\Users\\%USERNAME%\\.ollama\\models").expanduser(),
 27 | }
 28 | BACKUP_PATH = Path("~/Downloads/ollama_model_backups").expanduser()
 29 | 
 30 | 
 31 | def run_command(command: str | list) -> CMDOutput:
 32 |     process = Popen(
 33 |         command,
 34 |         shell=True,
 35 |         stdout=PIPE,
 36 |         stderr=PIPE,
 37 |         stdin=PIPE,
 38 |         text=True,
 39 |         encoding="utf-8",
 40 |     )
 41 | 
 42 |     output_text, error_text = process.communicate()
 43 | 
 44 |     return CMDOutput(
 45 |         output_text=output_text.strip(),
 46 |         error_text=error_text.strip(),
 47 |         return_code=process.returncode,
 48 |     )
 49 | 
 50 | 
 51 | def check_ollama_installed() -> bool:
 52 |     result = run_command("which ollama")
 53 |     return result.return_code == 0
 54 | 
 55 | 
 56 | def ollama_version() -> str:
 57 |     result = run_command("ollama --version")
 58 |     return result.output_text.strip()
 59 | 
 60 | 
 61 | def create_backup(path_to_backup: list[Path], backup_path: Path) -> None:
 62 |     with ZipFile(backup_path, "w") as zfile:
 63 |         for file in path_to_backup:
 64 |             zfile.write(file)
 65 | 
 66 | 
 67 | def ollama_models_path() -> Path:
 68 |     match platform.lower():
 69 |         case "linux":
 70 |             return MODELS_PATH["linux"]
 71 |         case "darwin":
 72 |             return MODELS_PATH["macos"]
 73 |         case "win32":
 74 |             return MODELS_PATH["windows"]
 75 |         case _:
 76 |             msg = "Unsupported operating system"
 77 |             raise OSError(msg)
 78 | 
 79 | 
 80 | def models() -> list[str]:
 81 |     result = run_command("ollama list").output_text.strip().split("\n")
 82 |     return [line.split()[0] for line in result[1:]]
 83 | 
 84 | 
 85 | def update_models(model_names: list[str]) -> None:
 86 |     for model_name in model_names:
 87 |         run_command(f"ollama pull {model_name}")
 88 | 
 89 | 
 90 | def backup_models(backup_path: Path = BACKUP_PATH, model: str | None = None) -> None:
 91 |     models_path = ollama_models_path()
 92 |     backup_path = Path(backup_path)
 93 |     backup_path.mkdir(parents=True, exist_ok=True)
 94 | 
 95 |     for model in models():
 96 |         model_name, model_version = (
 97 |             model.split(":") if ":" in model else (model, "latest")
 98 |         )
 99 |         model_schema_path = (
100 |             models_path
101 |             / f"manifests/registry.ollama.ai/library/{model_name}/{model_version}"
102 |         )
103 |         model_layers = loads(Path(model_schema_path).read_bytes())["layers"]
104 | 
105 |         digests_path = [
106 |             models_path / "blobs" / layer["digest"].replace(":", "-")
107 |             for layer in model_layers
108 |         ]
109 |         digests_path.append(model_schema_path)
110 | 
111 |         archive_path = backup_path / f"{model_name}-{model_version}.zip"
112 |         create_backup(digests_path, archive_path)
113 | 
114 | 
115 | def restore_models(backup_path: Path) -> None:
116 |     backup_path = Path(backup_path).expanduser()
117 |     models_path = ollama_models_path()
118 | 
119 |     with ZipFile(backup_path, "r") as zfile:
120 |         zfile.extractall(models_path)
121 | 
122 | 
123 | app = typer.Typer(no_args_is_help=True)
124 | 
125 | 
126 | def check_installation() -> None:
127 |     if not check_ollama_installed():
128 |         typer.echo(
129 |             "Error: Ollama is not installed. Please install Ollama to proceed.",
130 |             err=True,
131 |         )
132 |         raise typer.Exit(code=1)
133 | 
134 | 
135 | @app.command()
136 | def list() -> None:
137 |     """List all installed Ollama models."""
138 |     check_installation()
139 |     model_list = models()
140 | 
141 |     if not model_list:
142 |         typer.echo("No models installed.")
143 |         return
144 | 
145 |     typer.echo("\nInstalled Models:")
146 |     typer.echo("-" * 40)
147 |     for model in model_list:
148 |         typer.echo(f"  • {model}")
149 |     typer.echo("-" * 40)
150 |     typer.echo(f"\nTotal: {len(model_list)} model(s)")
151 | 
152 | 
153 | @app.command()
154 | def update(
155 |     model: str = typer.Argument(
156 |         None,
157 |         help="Model name to update (updates all if not provided)",
158 |     ),
159 | ) -> None:
160 |     """Update one or all Ollama models."""
161 |     check_installation()
162 | 
163 |     all_models = models()
164 |     models_to_update = [model] if model else all_models
165 | 
166 |     if not models_to_update:
167 |         typer.echo("No models to update.")
168 |         return
169 | 
170 |     typer.echo(f"Updating {len(models_to_update)} model(s)...\n")
171 |     update_models(models_to_update)
172 |     typer.echo("\nUpdate complete.")
173 | 
174 | 
175 | @app.command()
176 | def backup(
177 |     backup_path: Path = typer.Option(
178 |         BACKUP_PATH,
179 |         "--path",
180 |         "-p",
181 |         help="Directory to save backups (default: ~/Downloads/ollama_model_backups)",
182 |     ),
183 |     model: str = typer.Option(
184 |         None,
185 |         "--model",
186 |         "-m",
187 |         help="Specific model to backup (backs up all if not provided)",
188 |     ),
189 | ) -> None:
190 |     """Backup Ollama models to a zip file."""
191 |     check_installation()
192 | 
193 |     backup_path = Path(backup_path).expanduser()
194 |     typer.echo(f"Backing up models to: {backup_path}")
195 |     backup_models(backup_path, model)
196 |     typer.echo("\nBackup complete.")
197 | 
198 | 
199 | @app.command()
200 | def restore(
201 |     backup_path: Path = typer.Argument(
202 |         ...,
203 |         help="Path to backup zip file or directory",
204 |     ),
205 | ) -> None:
206 |     """Restore Ollama models from backup."""
207 |     check_installation()
208 | 
209 |     backup_path = Path(backup_path).expanduser()
210 |     if not backup_path.exists():
211 |         typer.echo(f"Error: Backup path does not exist: {backup_path}", err=True)
212 |         raise typer.Exit(code=1)
213 | 
214 |     typer.echo(f"Restoring models from: {backup_path}")
215 |     restore_models(backup_path)
216 |     typer.echo("\nRestore complete.")
217 | 
218 | 
219 | @app.command()
220 | def version() -> None:
221 |     """Show Ollama version."""
222 |     check_installation()
223 |     typer.echo(f"Ollama Version: {ollama_version()}")
224 | 
225 | 
226 | @app.command()
227 | def info() -> None:
228 |     """Show Ollama installation information."""
229 |     check_installation()
230 |     typer.echo(f"Ollama Version: {ollama_version()}")
231 |     typer.echo(f"Models Path: {ollama_models_path()}")
232 |     typer.echo(f"Platform: {platform}")
233 |     typer.echo(f"Installed Models: {len(models())}")
234 | 
235 | 
236 | @app.command()
237 | def check() -> None:
238 |     """Check if Ollama is installed and accessible."""
239 |     if check_ollama_installed():
240 |         typer.echo("✓ Ollama is installed and accessible")
241 |         typer.echo(f"  Version: {ollama_version()}")
242 |         typer.echo(f"  Models: {len(models())}")
243 |     else:
244 |         typer.echo("✗ Ollama is not installed or not accessible", err=True)
245 |         raise typer.Exit(code=1)
246 | 
247 | 
248 | def main() -> None:
249 |     app()
250 | 
251 | 
252 | if __name__ == "__main__":
253 |     main()
254 | 
```