lemonpie-cli 0.2.0a1__tar.gz → 0.2.0a3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lemonpie_cli-0.2.0a1/.github/workflows/publish.yml → lemonpie_cli-0.2.0a3/.github/workflows/ci.yml +3 -2
- lemonpie_cli-0.2.0a3/.github/workflows/release.yml +64 -0
- lemonpie_cli-0.2.0a3/.release-please/config.json +14 -0
- lemonpie_cli-0.2.0a3/.release-please/manifest.json +3 -0
- lemonpie_cli-0.2.0a3/CHANGELOG.md +35 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/PKG-INFO +1 -1
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/src/lemonpie/_version.py +2 -2
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/src/lemonpie/cli/config_cmd.py +2 -2
- lemonpie_cli-0.2.0a1/src/lemonpie/cli_utils.py → lemonpie_cli-0.2.0a3/src/lemonpie/cli/parser.py +1 -21
- lemonpie_cli-0.2.0a1/src/lemonpie/spinner.py → lemonpie_cli-0.2.0a3/src/lemonpie/cli/ui.py +31 -3
- lemonpie_cli-0.2.0a3/src/lemonpie/engine/__init__.py +0 -0
- lemonpie_cli-0.2.0a1/src/lemonpie/model_utils.py → lemonpie_cli-0.2.0a3/src/lemonpie/engine/models.py +29 -30
- lemonpie_cli-0.2.0a3/src/lemonpie/engine/ollama.py +49 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/src/lemonpie/main.py +40 -73
- lemonpie_cli-0.2.0a3/src/lemonpie/sessions/__init__.py +0 -0
- lemonpie_cli-0.2.0a1/src/lemonpie/session_utils.py → lemonpie_cli-0.2.0a3/src/lemonpie/sessions/storage.py +1 -2
- lemonpie_cli-0.2.0a1/.github/workflows/release.yml +0 -43
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/.gitignore +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/LICENSE +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/README.md +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/config.example.json +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/docs/design/state-machine.md +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/pyproject.toml +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/src/lemonpie/__init__.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/src/lemonpie/cli/__init__.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/src/lemonpie/config.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/src/lemonpie/paths.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/src/lemonpie/time_utils.py +0 -0
- {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0a3}/uv.lock +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Release & Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release-please:
|
|
15
|
+
name: Manage Release PR & Tags
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
outputs:
|
|
18
|
+
release_created: ${{ steps.release.outputs.releases_created }}
|
|
19
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
20
|
+
steps:
|
|
21
|
+
- uses: googleapis/release-please-action@v4
|
|
22
|
+
id: release
|
|
23
|
+
with:
|
|
24
|
+
config-file: .release-please/config.json
|
|
25
|
+
manifest-file: .release-please/manifest.json
|
|
26
|
+
|
|
27
|
+
pypi-publish:
|
|
28
|
+
name: Build and publish to PyPI
|
|
29
|
+
needs: release-please
|
|
30
|
+
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write # Required for Trusted Publisher OIDC authentication
|
|
35
|
+
|
|
36
|
+
steps:
|
|
37
|
+
- name: Check out repository
|
|
38
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
39
|
+
with:
|
|
40
|
+
fetch-depth: 0 # Required for hatch-vcs to inspect git history & tags
|
|
41
|
+
|
|
42
|
+
- name: Verify tag is reachable from main
|
|
43
|
+
run: |
|
|
44
|
+
git fetch origin main
|
|
45
|
+
if git merge-base --is-ancestor "${{ github.sha }}" origin/main; then
|
|
46
|
+
echo "Commit ${{ github.sha }} is on main. Proceeding."
|
|
47
|
+
else
|
|
48
|
+
echo "::error::Commit ${{ github.sha }} is not reachable from origin/main. Refusing to publish."
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
- name: Install uv
|
|
53
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
54
|
+
with:
|
|
55
|
+
version: "0.12.17"
|
|
56
|
+
|
|
57
|
+
- name: Set up Python
|
|
58
|
+
run: uv python install
|
|
59
|
+
|
|
60
|
+
- name: Build package artifacts
|
|
61
|
+
run: uv build
|
|
62
|
+
|
|
63
|
+
- name: Publish to PyPI
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"release-type": "python",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"package-name": "lemonpie-cli",
|
|
6
|
+
"changelog-path": "CHANGELOG.md",
|
|
7
|
+
"bump-minor-pre-major": true,
|
|
8
|
+
"bump-patch-for-minor-pre-major": true,
|
|
9
|
+
"prerelease": true,
|
|
10
|
+
"versioning": "prerelease",
|
|
11
|
+
"include-component-in-tag": false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0-alpha.3](https://github.com/famousStratum/LemonPie/compare/v0.2.0-alpha.2...v0.2.0-alpha.3) (2026-09-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* resolve tuple/dict mismatch in model alias lookup and dead code in session loading ([#23](https://github.com/famousStratum/LemonPie/issues/23)) ([65152bf](https://github.com/famousStratum/LemonPie/commit/65152bf9d6df0d91addad5aa72f73145383b99d9))
|
|
9
|
+
|
|
10
|
+
## [0.2.0-alpha.2](https://github.com/famousStratum/LemonPie/compare/lemonpie-cli-v0.2.0-alpha.1...lemonpie-cli-v0.2.0-alpha.2) (2026-09-21)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **cli:** enhance error handling, model defaults, timeouts, and connection status ([9147dc0](https://github.com/famousStratum/LemonPie/commit/9147dc0293065f0b2227ac8f36cdacf0502ca7c4))
|
|
16
|
+
* **cli:** improve connection error handling, timeouts, and documentation ([83e2659](https://github.com/famousStratum/LemonPie/commit/83e2659182717d82feb28be4b85d14e85679d751))
|
|
17
|
+
* **paths:** Depend on platformdirs for config/session paths; bump requires-python to >=3.11 ([6fbf8e9](https://github.com/famousStratum/LemonPie/commit/6fbf8e92d27cbec20c57f404aecf99940329b238))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **build:** explicitly specify hatchling wheel package directory ([705980a](https://github.com/famousStratum/LemonPie/commit/705980a9eda1672829119db0fcd2aab4a9c04101))
|
|
23
|
+
* **build:** specify hatchling package target ([b759f2d](https://github.com/famousStratum/LemonPie/commit/b759f2d086bccb7b39d456844d1bb7403410e8d4))
|
|
24
|
+
* **cli:** import missing resolve_session in main entrypoint ([f5c4c5a](https://github.com/famousStratum/LemonPie/commit/f5c4c5ae1cc7ade21ba41eb92a9ecc640a7fe969))
|
|
25
|
+
* **cli:** reorder main entrypoint control flow and fix argument handling ([3a182d2](https://github.com/famousStratum/LemonPie/commit/3a182d2485af36f8a98a28cb41459f968fc9f4d4))
|
|
26
|
+
* **cli:** reorder main entrypoint control flow and resolve model assignment ([ad944f9](https://github.com/famousStratum/LemonPie/commit/ad944f93e2661eb3f2cc1abfe6cdeedc04f6ee86))
|
|
27
|
+
* **cli:** resolve NameError in main entrypoint ([532d661](https://github.com/famousStratum/LemonPie/commit/532d6617ff589de5f5def690e6390b1341517f84))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
* Add roadmap section to README and initial state-machine design doc ([fa9b000](https://github.com/famousStratum/LemonPie/commit/fa9b0009aaca85cbcd3e5a6f1e1c6c4d3bc40196))
|
|
33
|
+
* update README for src/ layout and platformdirs ([a15cde2](https://github.com/famousStratum/LemonPie/commit/a15cde25a08c49d11366bc64c31aa80da9590838))
|
|
34
|
+
* update README.md ([cf7a046](https://github.com/famousStratum/LemonPie/commit/cf7a0468474cc6bb47dbf33148a6b18d11b44fdf))
|
|
35
|
+
* update roadmap in README.md ([d0a3316](https://github.com/famousStratum/LemonPie/commit/d0a33167361efd400682f54780aaf3fff738e41d))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lemonpie-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.0a3
|
|
4
4
|
Summary: LemonPie — A lightweight Ollama client for terminal users
|
|
5
5
|
Project-URL: homepage, https://versicode.com/lemonpie
|
|
6
6
|
Project-URL: repository, https://github.com/famousStratum/LemonPie
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2, 0, '
|
|
21
|
+
__version__ = version = '0.2.0a3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'a3')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -15,8 +15,8 @@ import argparse, json, os, sys
|
|
|
15
15
|
from urllib.parse import urlparse
|
|
16
16
|
|
|
17
17
|
from lemonpie.paths import CONFIG_FILE
|
|
18
|
-
from lemonpie.
|
|
19
|
-
from lemonpie.
|
|
18
|
+
from lemonpie.cli.ui import confirm
|
|
19
|
+
from lemonpie.engine.models import (
|
|
20
20
|
find_by_alias,
|
|
21
21
|
find_by_name,
|
|
22
22
|
)
|
lemonpie_cli-0.2.0a1/src/lemonpie/cli_utils.py → lemonpie_cli-0.2.0a3/src/lemonpie/cli/parser.py
RENAMED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
|
|
3
|
-
from lemonpie.session_utils import list_sessions, read_current
|
|
4
|
-
|
|
5
3
|
try:
|
|
6
4
|
from lemonpie._version import __version__
|
|
7
5
|
except ImportError:
|
|
8
6
|
__version__ = "0.0.0.dev0"
|
|
9
7
|
|
|
8
|
+
|
|
10
9
|
def build_parser():
|
|
11
10
|
parser = argparse.ArgumentParser(prog="lm", description="Run LemonPie with a given model or prompt")
|
|
12
11
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
@@ -21,22 +20,3 @@ def build_parser():
|
|
|
21
20
|
parser.add_argument("--delete-all", action="store_true", help="Delete all stored sessions")
|
|
22
21
|
parser.add_argument("prompt", nargs="*", help="Prompt text")
|
|
23
22
|
return parser
|
|
24
|
-
|
|
25
|
-
def confirm(prompt):
|
|
26
|
-
ans = input(f"{prompt} (y/N): ").strip().lower()
|
|
27
|
-
return ans in ("y", "yes")
|
|
28
|
-
|
|
29
|
-
def print_session(session):
|
|
30
|
-
print(f"Session {session['id']} — {session.get('title') or '<no title>'}")
|
|
31
|
-
for turn in session["history"]:
|
|
32
|
-
role = turn["role"]
|
|
33
|
-
content = turn["content"]
|
|
34
|
-
print(f"{role} [{turn.get('ts','')}] : {content}")
|
|
35
|
-
|
|
36
|
-
def print_sessions_list():
|
|
37
|
-
sessions = list_sessions()
|
|
38
|
-
if not sessions:
|
|
39
|
-
print("No sessions found.")
|
|
40
|
-
return
|
|
41
|
-
for sid, title, created, updated, is_current in sessions:
|
|
42
|
-
print(f"{is_current} {sid} {title} created: {created}, updated: {updated}")
|
|
@@ -1,17 +1,45 @@
|
|
|
1
|
-
import
|
|
1
|
+
import random
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from lemonpie.sessions.storage import list_sessions
|
|
2
7
|
|
|
3
8
|
CONNECTING_MSGS = [
|
|
4
9
|
"Connecting",
|
|
5
10
|
"Squeezing the lemons",
|
|
6
|
-
"Knocking on Ollama’s door"
|
|
11
|
+
"Knocking on Ollama’s door",
|
|
7
12
|
]
|
|
8
13
|
|
|
9
14
|
WAITING_MSGS = [
|
|
10
15
|
"Waiting for model response",
|
|
11
16
|
"The pie is cooking",
|
|
12
|
-
"Brewing tokens"
|
|
17
|
+
"Brewing tokens",
|
|
13
18
|
]
|
|
14
19
|
|
|
20
|
+
|
|
21
|
+
def confirm(prompt):
|
|
22
|
+
ans = input(f"{prompt} (y/N): ").strip().lower()
|
|
23
|
+
return ans in ("y", "yes")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def print_session(session):
|
|
27
|
+
print(f"Session {session['id']} — {session.get('title') or '<no title>'}")
|
|
28
|
+
for turn in session["history"]:
|
|
29
|
+
role = turn["role"]
|
|
30
|
+
content = turn["content"]
|
|
31
|
+
print(f"{role} [{turn.get('ts', '')}] : {content}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def print_sessions_list():
|
|
35
|
+
sessions = list_sessions()
|
|
36
|
+
if not sessions:
|
|
37
|
+
print("No sessions found.")
|
|
38
|
+
return
|
|
39
|
+
for sid, title, created, updated, is_current in sessions:
|
|
40
|
+
print(f"{is_current} {sid} {title} created: {created}, updated: {updated}")
|
|
41
|
+
|
|
42
|
+
|
|
15
43
|
class StatusSpinner:
|
|
16
44
|
def __init__(self, messages, interval=0.5):
|
|
17
45
|
self.messages = messages
|
|
File without changes
|
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
|
|
3
|
+
from lemonpie.sessions.storage import save_session
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
def find_by_alias(cfg, alias):
|
|
7
|
+
for idx, m in enumerate(cfg.get("models", [])):
|
|
8
|
+
if m.get("alias") == alias:
|
|
9
|
+
return idx, m
|
|
10
|
+
return None, None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_by_name(cfg, name):
|
|
14
|
+
for idx, m in enumerate(cfg.get("models", [])):
|
|
15
|
+
if m.get("name") == name:
|
|
16
|
+
return idx, m
|
|
17
|
+
return None, None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_model(cfg, args_model, default_model_name):
|
|
21
|
+
if args_model:
|
|
22
|
+
_, alias_entry = find_by_alias(cfg, args_model)
|
|
23
|
+
if alias_entry:
|
|
24
|
+
return alias_entry["name"]
|
|
25
|
+
_, name_entry = find_by_name(cfg, args_model)
|
|
26
|
+
if name_entry:
|
|
27
|
+
return name_entry["name"]
|
|
28
|
+
return args_model
|
|
29
|
+
return default_model_name
|
|
5
30
|
|
|
6
31
|
|
|
7
32
|
def handle_model_switch(cfg, session, requested_model):
|
|
8
33
|
"""
|
|
9
|
-
Handles validating a requested model/alias and updates the active session
|
|
34
|
+
Handles validating a requested model/alias and updates the active session
|
|
10
35
|
if confirmed by the user.
|
|
11
36
|
"""
|
|
12
37
|
if not session or not requested_model:
|
|
13
38
|
return session
|
|
14
39
|
|
|
15
|
-
alias_entry = find_by_alias(cfg, requested_model)
|
|
16
|
-
name_entry = find_by_name(cfg, requested_model)
|
|
40
|
+
_, alias_entry = find_by_alias(cfg, requested_model)
|
|
41
|
+
_, name_entry = find_by_name(cfg, requested_model)
|
|
17
42
|
|
|
18
43
|
if alias_entry:
|
|
19
44
|
new_model = alias_entry["name"]
|
|
@@ -34,29 +59,3 @@ def handle_model_switch(cfg, session, requested_model):
|
|
|
34
59
|
print("Aborted model change.")
|
|
35
60
|
|
|
36
61
|
return session
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def find_by_alias(cfg, alias):
|
|
40
|
-
for idx, m in enumerate(cfg.get("models", [])):
|
|
41
|
-
if m.get("alias") == alias:
|
|
42
|
-
return idx, m
|
|
43
|
-
return None, None
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def find_by_name(cfg, name):
|
|
47
|
-
for idx, m in enumerate(cfg.get("models", [])):
|
|
48
|
-
if m.get("name") == name:
|
|
49
|
-
return idx, m
|
|
50
|
-
return None, None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def resolve_model(cfg, args_model, default_model_name):
|
|
54
|
-
if args_model:
|
|
55
|
-
alias_entry = find_by_alias(cfg, args_model)
|
|
56
|
-
if alias_entry:
|
|
57
|
-
return alias_entry["name"]
|
|
58
|
-
name_entry = find_by_name(cfg, args_model)
|
|
59
|
-
if name_entry:
|
|
60
|
-
return name_entry["name"]
|
|
61
|
-
return args_model
|
|
62
|
-
return default_model_name
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Ollama API interaction abstractions: client construction and streaming chat."""
|
|
2
|
+
|
|
3
|
+
from ollama import Client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_client(server_host, timeout):
|
|
7
|
+
return Client(host=server_host, timeout=timeout)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stream_chat(client, model, messages):
|
|
11
|
+
"""
|
|
12
|
+
Stream a chat completion from Ollama, yielding text content chunks.
|
|
13
|
+
|
|
14
|
+
The underlying stream is closed automatically when this generator is
|
|
15
|
+
closed (e.g. via a `for` loop finishing, an exception propagating, or an
|
|
16
|
+
explicit `.close()` call on the generator by the caller).
|
|
17
|
+
"""
|
|
18
|
+
stream = client.chat(model=model, messages=messages, stream=True)
|
|
19
|
+
try:
|
|
20
|
+
for chunk in stream:
|
|
21
|
+
# Standard Ollama client attribute access
|
|
22
|
+
content = getattr(getattr(chunk, "message", None), "content", None)
|
|
23
|
+
if content is None and isinstance(chunk, dict):
|
|
24
|
+
content = chunk.get("message", {}).get("content", "")
|
|
25
|
+
if content:
|
|
26
|
+
yield content
|
|
27
|
+
finally:
|
|
28
|
+
if hasattr(stream, "close"):
|
|
29
|
+
stream.close()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_ollama_error(exc, server_host):
|
|
33
|
+
"""
|
|
34
|
+
Map an exception raised during an Ollama request to a (message, hint)
|
|
35
|
+
pair suitable for printing to the user. `hint` may be None.
|
|
36
|
+
"""
|
|
37
|
+
msg = str(exc)
|
|
38
|
+
if "[Errno 111]" in msg or "Connection refused" in msg:
|
|
39
|
+
return (
|
|
40
|
+
"Ollama server did not respond: Connection refused.",
|
|
41
|
+
f"Hint: Ollama may not be running, or it may not be listening on: {server_host}\n"
|
|
42
|
+
"Check that the server is started with `ollama serve` and bound to the correct interface.",
|
|
43
|
+
)
|
|
44
|
+
if "[Errno 110]" in msg or "timed out" in msg.lower():
|
|
45
|
+
return (
|
|
46
|
+
"Failed to contact Ollama server: Connection timed out.",
|
|
47
|
+
f"Hint: The server at {server_host} is unreachable. Verify IP/port and network.",
|
|
48
|
+
)
|
|
49
|
+
return (f"Failed to contact Ollama server: {msg}", None)
|
|
@@ -17,11 +17,7 @@ Quick usage
|
|
|
17
17
|
See README.md for details and maintainer notes.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
import
|
|
21
|
-
from datetime import datetime, timezone
|
|
22
|
-
from ollama import Client
|
|
23
|
-
|
|
24
|
-
from lemonpie.paths import CURRENT_FILE, SESSIONS_DIR
|
|
20
|
+
import sys
|
|
25
21
|
|
|
26
22
|
from lemonpie.config import (
|
|
27
23
|
load_config,
|
|
@@ -29,55 +25,47 @@ from lemonpie.config import (
|
|
|
29
25
|
build_timeout,
|
|
30
26
|
)
|
|
31
27
|
|
|
32
|
-
from lemonpie.
|
|
33
|
-
|
|
34
|
-
list_sessions,
|
|
28
|
+
from lemonpie.cli.parser import build_parser
|
|
29
|
+
from lemonpie.cli.ui import (
|
|
35
30
|
print_session,
|
|
36
31
|
print_sessions_list,
|
|
32
|
+
StatusSpinner,
|
|
33
|
+
CONNECTING_MSGS,
|
|
34
|
+
WAITING_MSGS,
|
|
37
35
|
)
|
|
38
36
|
|
|
39
|
-
from lemonpie.
|
|
40
|
-
find_by_alias,
|
|
37
|
+
from lemonpie.engine.models import (
|
|
41
38
|
find_by_name,
|
|
42
39
|
handle_model_switch,
|
|
43
40
|
resolve_model,
|
|
44
41
|
)
|
|
45
42
|
|
|
46
|
-
from lemonpie.
|
|
43
|
+
from lemonpie.engine.ollama import (
|
|
44
|
+
build_client,
|
|
45
|
+
stream_chat,
|
|
46
|
+
resolve_ollama_error,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from lemonpie.sessions.storage import (
|
|
47
50
|
create_session,
|
|
48
|
-
load_session_by_id,
|
|
49
51
|
delete_session_by_id,
|
|
50
52
|
delete_all_sessions,
|
|
51
53
|
resolve_session,
|
|
52
54
|
close_session,
|
|
53
55
|
load_session,
|
|
54
56
|
save_session,
|
|
55
|
-
delete_session,
|
|
56
57
|
read_current,
|
|
57
|
-
write_current,
|
|
58
|
-
clear_current,
|
|
59
|
-
default_title,
|
|
60
|
-
set_title,
|
|
61
58
|
update_title,
|
|
62
59
|
)
|
|
63
60
|
|
|
64
|
-
from lemonpie.spinner import (
|
|
65
|
-
StatusSpinner,
|
|
66
|
-
CONNECTING_MSGS,
|
|
67
|
-
WAITING_MSGS,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
61
|
from lemonpie.time_utils import now_ts
|
|
71
62
|
|
|
72
|
-
cfg = load_config()
|
|
73
|
-
server_host = resolve_server_host(cfg)
|
|
74
|
-
timeout = build_timeout()
|
|
75
|
-
client = Client(host=server_host, timeout=timeout)
|
|
76
|
-
|
|
77
|
-
models_list = cfg["models"]
|
|
78
|
-
default_model_name = cfg.get("default") or None
|
|
79
|
-
|
|
80
63
|
def main():
|
|
64
|
+
cfg = load_config()
|
|
65
|
+
server_host = resolve_server_host(cfg)
|
|
66
|
+
timeout = build_timeout()
|
|
67
|
+
client = build_client(server_host, timeout)
|
|
68
|
+
default_model_name = cfg.get("default") or None
|
|
81
69
|
parser = build_parser()
|
|
82
70
|
args = parser.parse_args()
|
|
83
71
|
|
|
@@ -85,18 +73,9 @@ def main():
|
|
|
85
73
|
parser.print_help()
|
|
86
74
|
sys.exit(1)
|
|
87
75
|
|
|
88
|
-
model_flag = any(a in ("-m", "--model") for a in sys.argv[1:])
|
|
89
|
-
resolve_model(cfg, args.model, default_model_name)
|
|
90
|
-
|
|
91
|
-
model = default_model_name
|
|
92
|
-
if model is None:
|
|
93
|
-
print("No default model configured.")
|
|
94
|
-
print("Use lmConfig to add one or pass a known alias with -m.")
|
|
95
|
-
sys.exit(1)
|
|
96
|
-
|
|
97
76
|
current_id = read_current()
|
|
98
77
|
|
|
99
|
-
|
|
78
|
+
# Direct CLI actions
|
|
100
79
|
|
|
101
80
|
if args.list:
|
|
102
81
|
print_sessions_list(); sys.exit(0)
|
|
@@ -122,10 +101,18 @@ def main():
|
|
|
122
101
|
confirm = input("Are you sure you want to delete ALL sessions? (y/N): ")
|
|
123
102
|
if confirm.lower() in ("y", "yes"):
|
|
124
103
|
count = delete_all_sessions()
|
|
125
|
-
print(f"Deleted {count} session(s).")
|
|
104
|
+
print(f"Deleted {count} session(s)."); sys.exit(0)
|
|
126
105
|
else:
|
|
127
106
|
print("Aborted."); sys.exit(0)
|
|
128
107
|
|
|
108
|
+
model_flag = args.model is not None
|
|
109
|
+
model = resolve_model(cfg, args.model, default_model_name)
|
|
110
|
+
|
|
111
|
+
if model is None:
|
|
112
|
+
print("No default model configured.")
|
|
113
|
+
print("Use lmConfig to add one or pass a known alias with -m.")
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
|
|
129
116
|
if model_flag and not current_id and not args.new and not args.session and not args.prompt:
|
|
130
117
|
print(f'No current session.\n'
|
|
131
118
|
f'To create a new session with "{args.model}" use: lm -n -m {args.model}\n'
|
|
@@ -141,6 +128,8 @@ def main():
|
|
|
141
128
|
|
|
142
129
|
if args.title:
|
|
143
130
|
session = update_title(session, model, args.title, args.prompt)
|
|
131
|
+
if not args.prompt:
|
|
132
|
+
sys.exit(0)
|
|
144
133
|
|
|
145
134
|
if args.prompt:
|
|
146
135
|
user_prompt = " ".join(args.prompt)
|
|
@@ -163,50 +152,28 @@ def main():
|
|
|
163
152
|
assistant_reply = ""
|
|
164
153
|
reply_ts = None
|
|
165
154
|
first_chunk_seen = False
|
|
155
|
+
chunks = stream_chat(client, model, session["history"])
|
|
166
156
|
|
|
167
157
|
try:
|
|
168
|
-
|
|
169
|
-
spinner.stop()
|
|
170
|
-
|
|
171
|
-
spinner = StatusSpinner(WAITING_MSGS)
|
|
172
|
-
spinner.start()
|
|
173
|
-
|
|
174
|
-
for chunk in stream:
|
|
158
|
+
for content in chunks:
|
|
175
159
|
if not first_chunk_seen:
|
|
176
160
|
spinner.stop()
|
|
177
161
|
first_chunk_seen = True
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
content = getattr(getattr(chunk, "message", None), "content", None)
|
|
181
|
-
if content is None and isinstance(chunk, dict):
|
|
182
|
-
content = chunk.get("message", {}).get("content", "")
|
|
183
|
-
|
|
184
|
-
if content:
|
|
185
|
-
print(content, end="", flush=True)
|
|
186
|
-
assistant_reply += content
|
|
162
|
+
print(content, end="", flush=True)
|
|
163
|
+
assistant_reply += content
|
|
187
164
|
print()
|
|
188
165
|
reply_ts = now_ts()
|
|
189
166
|
|
|
190
167
|
except KeyboardInterrupt:
|
|
191
|
-
|
|
192
|
-
if 'stream' in locals() and hasattr(stream, "close"):
|
|
193
|
-
stream.close()
|
|
194
|
-
except Exception:
|
|
195
|
-
pass
|
|
168
|
+
chunks.close()
|
|
196
169
|
print("\nInterrupted by user.")
|
|
197
170
|
sys.exit(1)
|
|
198
171
|
|
|
199
172
|
except Exception as e:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
print(
|
|
204
|
-
print("Check that the server is started with `ollama serve` and bound to the correct interface.")
|
|
205
|
-
elif "[Errno 110]" in msg or "timed out" in msg.lower():
|
|
206
|
-
print("\nFailed to contact Ollama server: Connection timed out.")
|
|
207
|
-
print("Hint: The server at", server_host, "is unreachable. Verify IP/port and network.")
|
|
208
|
-
else:
|
|
209
|
-
print("\nFailed to contact Ollama server:", msg)
|
|
173
|
+
title, hint = resolve_ollama_error(e, server_host)
|
|
174
|
+
print("\n" + title)
|
|
175
|
+
if hint:
|
|
176
|
+
print(hint)
|
|
210
177
|
sys.exit(1)
|
|
211
178
|
|
|
212
179
|
finally:
|
|
File without changes
|
|
@@ -41,7 +41,7 @@ def resolve_session(args, model, current_id):
|
|
|
41
41
|
def load_session(session_id):
|
|
42
42
|
try:
|
|
43
43
|
with open(session_path(session_id), "r", encoding="utf-8") as f:
|
|
44
|
-
|
|
44
|
+
s = json.load(f)
|
|
45
45
|
# Backfill created_at/updated_at from history if missing
|
|
46
46
|
if "created_at" not in s or not s.get("created_at"):
|
|
47
47
|
if s.get("history"):
|
|
@@ -179,6 +179,5 @@ def list_sessions():
|
|
|
179
179
|
sessions.append((data["id"], data.get("title") or "<no title>", created, updated, is_current))
|
|
180
180
|
except Exception:
|
|
181
181
|
# skip invalid JSON files
|
|
182
|
-
print("DEBUG: loaded", fname, "->", data.get("id"))
|
|
183
182
|
continue
|
|
184
183
|
return sessions
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
name: Release Package to PyPI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- "v*"
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
pypi-publish:
|
|
10
|
-
name: Build and publish to PyPI
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
environment: pypi
|
|
13
|
-
permissions:
|
|
14
|
-
id-token: write # Required for Trusted Publisher OIDC authentication
|
|
15
|
-
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
18
|
-
with:
|
|
19
|
-
fetch-depth: 0
|
|
20
|
-
|
|
21
|
-
- name: Verify tag is reachable from main
|
|
22
|
-
run: |
|
|
23
|
-
git fetch origin main
|
|
24
|
-
if git merge-base --is-ancestor "${{ github.sha }}" origin/main; then
|
|
25
|
-
echo "Tag ${{ github.ref_name }} (commit ${{ github.sha }}) is on main. Proceeding."
|
|
26
|
-
else
|
|
27
|
-
echo "::error::Tag ${{ github.ref_name }} (commit ${{ github.sha }}) is not reachable from origin/main. Refusing to publish — merge to main before tagging."
|
|
28
|
-
exit 1
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
- name: Install uv
|
|
32
|
-
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
33
|
-
with:
|
|
34
|
-
version: "0.12.17"
|
|
35
|
-
|
|
36
|
-
- name: Set up Python
|
|
37
|
-
run: uv python install
|
|
38
|
-
|
|
39
|
-
- name: Build package artifacts
|
|
40
|
-
run: uv build
|
|
41
|
-
|
|
42
|
-
- name: Publish to PyPI
|
|
43
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|