lemonpie-cli 0.2.0a1__tar.gz → 0.2.0b1__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.
Files changed (32) hide show
  1. lemonpie_cli-0.2.0a1/.github/workflows/publish.yml → lemonpie_cli-0.2.0b1/.github/workflows/ci.yml +3 -2
  2. lemonpie_cli-0.2.0b1/.github/workflows/release.yml +65 -0
  3. lemonpie_cli-0.2.0b1/.release-please/config.json +14 -0
  4. lemonpie_cli-0.2.0b1/.release-please/manifest.json +3 -0
  5. lemonpie_cli-0.2.0b1/CHANGELOG.md +42 -0
  6. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/PKG-INFO +1 -1
  7. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/pyproject.toml +7 -0
  8. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/_version.py +2 -2
  9. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/cli/config_cmd.py +8 -3
  10. lemonpie_cli-0.2.0a1/src/lemonpie/cli_utils.py → lemonpie_cli-0.2.0b1/src/lemonpie/cli/parser.py +1 -21
  11. lemonpie_cli-0.2.0a1/src/lemonpie/spinner.py → lemonpie_cli-0.2.0b1/src/lemonpie/cli/ui.py +31 -3
  12. lemonpie_cli-0.2.0b1/src/lemonpie/engine/__init__.py +0 -0
  13. lemonpie_cli-0.2.0a1/src/lemonpie/model_utils.py → lemonpie_cli-0.2.0b1/src/lemonpie/engine/models.py +29 -30
  14. lemonpie_cli-0.2.0b1/src/lemonpie/engine/ollama.py +49 -0
  15. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/main.py +43 -81
  16. lemonpie_cli-0.2.0b1/src/lemonpie/sessions/__init__.py +0 -0
  17. lemonpie_cli-0.2.0a1/src/lemonpie/session_utils.py → lemonpie_cli-0.2.0b1/src/lemonpie/sessions/storage.py +10 -19
  18. lemonpie_cli-0.2.0b1/tests/conftest.py +84 -0
  19. lemonpie_cli-0.2.0b1/tests/test_cli.py +110 -0
  20. lemonpie_cli-0.2.0b1/tests/test_sessions.py +294 -0
  21. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/uv.lock +69 -0
  22. lemonpie_cli-0.2.0a1/.github/workflows/release.yml +0 -43
  23. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/.gitignore +0 -0
  24. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/LICENSE +0 -0
  25. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/README.md +0 -0
  26. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/config.example.json +0 -0
  27. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/docs/design/state-machine.md +0 -0
  28. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/__init__.py +0 -0
  29. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/cli/__init__.py +0 -0
  30. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/config.py +0 -0
  31. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/paths.py +0 -0
  32. {lemonpie_cli-0.2.0a1 → lemonpie_cli-0.2.0b1}/src/lemonpie/time_utils.py +0 -0
@@ -4,8 +4,6 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - main
7
- tags:
8
- - "v*"
9
7
  pull_request:
10
8
  branches:
11
9
  - main
@@ -29,3 +27,6 @@ jobs:
29
27
 
30
28
  - name: Verify build artifact
31
29
  run: uv build
30
+
31
+ - name: Run tests
32
+ run: uv run pytest
@@ -0,0 +1,65 @@
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
+ token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
25
+ config-file: .release-please/config.json
26
+ manifest-file: .release-please/manifest.json
27
+
28
+ pypi-publish:
29
+ name: Build and publish to PyPI
30
+ needs: release-please
31
+ if: ${{ needs.release-please.outputs.release_created == 'true' }}
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ id-token: write # Required for Trusted Publisher OIDC authentication
36
+
37
+ steps:
38
+ - name: Check out repository
39
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40
+ with:
41
+ fetch-depth: 0 # Required for hatch-vcs to inspect git history & tags
42
+
43
+ - name: Verify tag is reachable from main
44
+ run: |
45
+ git fetch origin main
46
+ if git merge-base --is-ancestor "${{ github.sha }}" origin/main; then
47
+ echo "Commit ${{ github.sha }} is on main. Proceeding."
48
+ else
49
+ echo "::error::Commit ${{ github.sha }} is not reachable from origin/main. Refusing to publish."
50
+ exit 1
51
+ fi
52
+
53
+ - name: Install uv
54
+ uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
55
+ with:
56
+ version: "0.12.17"
57
+
58
+ - name: Set up Python
59
+ run: uv python install
60
+
61
+ - name: Build package artifacts
62
+ run: uv build
63
+
64
+ - name: Publish to PyPI
65
+ 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,3 @@
1
+ {
2
+ ".": "0.2.0-beta.1"
3
+ }
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0-beta.1](https://github.com/famousStratum/LemonPie/compare/v0.2.0-alpha.3...v0.2.0-beta.1) (2026-09-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * eliminate now_ts() double-calls and session ID collisions in session creation ([#27](https://github.com/famousStratum/LemonPie/issues/27)) ([7864136](https://github.com/famousStratum/LemonPie/commit/7864136b6dc200e1e682b61252eadc74e99ac66a))
9
+
10
+ ## [0.2.0-alpha.3](https://github.com/famousStratum/LemonPie/compare/v0.2.0-alpha.2...v0.2.0-alpha.3) (2026-09-21)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * 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))
16
+
17
+ ## [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)
18
+
19
+
20
+ ### Features
21
+
22
+ * **cli:** enhance error handling, model defaults, timeouts, and connection status ([9147dc0](https://github.com/famousStratum/LemonPie/commit/9147dc0293065f0b2227ac8f36cdacf0502ca7c4))
23
+ * **cli:** improve connection error handling, timeouts, and documentation ([83e2659](https://github.com/famousStratum/LemonPie/commit/83e2659182717d82feb28be4b85d14e85679d751))
24
+ * **paths:** Depend on platformdirs for config/session paths; bump requires-python to >=3.11 ([6fbf8e9](https://github.com/famousStratum/LemonPie/commit/6fbf8e92d27cbec20c57f404aecf99940329b238))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **build:** explicitly specify hatchling wheel package directory ([705980a](https://github.com/famousStratum/LemonPie/commit/705980a9eda1672829119db0fcd2aab4a9c04101))
30
+ * **build:** specify hatchling package target ([b759f2d](https://github.com/famousStratum/LemonPie/commit/b759f2d086bccb7b39d456844d1bb7403410e8d4))
31
+ * **cli:** import missing resolve_session in main entrypoint ([f5c4c5a](https://github.com/famousStratum/LemonPie/commit/f5c4c5ae1cc7ade21ba41eb92a9ecc640a7fe969))
32
+ * **cli:** reorder main entrypoint control flow and fix argument handling ([3a182d2](https://github.com/famousStratum/LemonPie/commit/3a182d2485af36f8a98a28cb41459f968fc9f4d4))
33
+ * **cli:** reorder main entrypoint control flow and resolve model assignment ([ad944f9](https://github.com/famousStratum/LemonPie/commit/ad944f93e2661eb3f2cc1abfe6cdeedc04f6ee86))
34
+ * **cli:** resolve NameError in main entrypoint ([532d661](https://github.com/famousStratum/LemonPie/commit/532d6617ff589de5f5def690e6390b1341517f84))
35
+
36
+
37
+ ### Documentation
38
+
39
+ * Add roadmap section to README and initial state-machine design doc ([fa9b000](https://github.com/famousStratum/LemonPie/commit/fa9b0009aaca85cbcd3e5a6f1e1c6c4d3bc40196))
40
+ * update README for src/ layout and platformdirs ([a15cde2](https://github.com/famousStratum/LemonPie/commit/a15cde25a08c49d11366bc64c31aa80da9590838))
41
+ * update README.md ([cf7a046](https://github.com/famousStratum/LemonPie/commit/cf7a0468474cc6bb47dbf33148a6b18d11b44fdf))
42
+ * 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.0a1
3
+ Version: 0.2.0b1
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
@@ -37,3 +37,10 @@ version-file = "src/lemonpie/_version.py"
37
37
 
38
38
  [tool.hatch.build.targets.wheel]
39
39
  packages = ["src/lemonpie"]
40
+
41
+ [dependency-groups]
42
+ dev = ["pytest>=8.0"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ addopts = "-ra"
@@ -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.0a1'
22
- __version_tuple__ = version_tuple = (0, 2, 0, 'a1')
21
+ __version__ = version = '0.2.0b1'
22
+ __version_tuple__ = version_tuple = (0, 2, 0, 'b1')
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.cli_utils import confirm
19
- from lemonpie.model_utils import (
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
  )
@@ -196,7 +196,7 @@ def cmd_show(cfg):
196
196
  return 0
197
197
 
198
198
 
199
- def main():
199
+ def build_parser():
200
200
  parser = argparse.ArgumentParser(prog="lmConfig", description="Manage LemonPie configuration (config.json)")
201
201
  parser.add_argument("--host", help="Set Ollama host URL (http(s)://host:port)")
202
202
  parser.add_argument("--default-model", help="Set default model using alias or full name (must exist in models)")
@@ -205,6 +205,11 @@ def main():
205
205
  parser.add_argument("--list-models", action="store_true", help="List configured model aliases and names")
206
206
  parser.add_argument("--show", action="store_true", help="Print full config.json")
207
207
  parser.add_argument("--force", action="store_true", help="Force overwrite when adding a model")
208
+ return parser
209
+
210
+
211
+ def main():
212
+ parser = build_parser()
208
213
  args = parser.parse_args()
209
214
 
210
215
  cfg = load_config()
@@ -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 sys, time, random, threading
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
- from lemonpie.session_utils import save_session
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 argparse, errno, httpx, json, os, sys
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,46 @@ from lemonpie.config import (
29
25
  build_timeout,
30
26
  )
31
27
 
32
- from lemonpie.cli_utils import (
33
- build_parser,
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.model_utils import (
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.session_utils import (
47
- create_session,
48
- load_session_by_id,
43
+ from lemonpie.engine.ollama import (
44
+ build_client,
45
+ stream_chat,
46
+ resolve_ollama_error,
47
+ )
48
+
49
+ from lemonpie.sessions.storage import (
49
50
  delete_session_by_id,
50
51
  delete_all_sessions,
51
52
  resolve_session,
52
53
  close_session,
53
54
  load_session,
54
55
  save_session,
55
- delete_session,
56
56
  read_current,
57
- write_current,
58
- clear_current,
59
- default_title,
60
- set_title,
61
57
  update_title,
62
58
  )
63
59
 
64
- from lemonpie.spinner import (
65
- StatusSpinner,
66
- CONNECTING_MSGS,
67
- WAITING_MSGS,
68
- )
69
-
70
60
  from lemonpie.time_utils import now_ts
71
61
 
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
62
  def main():
63
+ cfg = load_config()
64
+ server_host = resolve_server_host(cfg)
65
+ timeout = build_timeout()
66
+ client = build_client(server_host, timeout)
67
+ default_model_name = cfg.get("default") or None
81
68
  parser = build_parser()
82
69
  args = parser.parse_args()
83
70
 
@@ -85,18 +72,9 @@ def main():
85
72
  parser.print_help()
86
73
  sys.exit(1)
87
74
 
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
75
  current_id = read_current()
98
76
 
99
- # Direct CLI actions
77
+ # Direct CLI actions
100
78
 
101
79
  if args.list:
102
80
  print_sessions_list(); sys.exit(0)
@@ -122,10 +100,18 @@ def main():
122
100
  confirm = input("Are you sure you want to delete ALL sessions? (y/N): ")
123
101
  if confirm.lower() in ("y", "yes"):
124
102
  count = delete_all_sessions()
125
- print(f"Deleted {count} session(s).")
103
+ print(f"Deleted {count} session(s)."); sys.exit(0)
126
104
  else:
127
105
  print("Aborted."); sys.exit(0)
128
106
 
107
+ model_flag = args.model is not None
108
+ model = resolve_model(cfg, args.model, default_model_name)
109
+
110
+ if model is None:
111
+ print("No default model configured.")
112
+ print("Use lmConfig to add one or pass a known alias with -m.")
113
+ sys.exit(1)
114
+
129
115
  if model_flag and not current_id and not args.new and not args.session and not args.prompt:
130
116
  print(f'No current session.\n'
131
117
  f'To create a new session with "{args.model}" use: lm -n -m {args.model}\n'
@@ -141,17 +127,15 @@ def main():
141
127
 
142
128
  if args.title:
143
129
  session = update_title(session, model, args.title, args.prompt)
130
+ if not args.prompt:
131
+ sys.exit(0)
144
132
 
145
133
  if args.prompt:
146
134
  user_prompt = " ".join(args.prompt)
147
135
  turn_ts = now_ts()
148
- if session:
149
- session["history"].append({"role": "user", "content": user_prompt, "ts": turn_ts})
150
- session["updated_at"] = turn_ts
151
- save_session(session_id, session)
152
- else:
153
- session = create_session(user_prompt, model)
154
- session_id = session["id"]
136
+ session["history"].append({"role": "user", "content": user_prompt, "ts": turn_ts})
137
+ session["updated_at"] = turn_ts
138
+ save_session(session_id, session)
155
139
 
156
140
  if not find_by_name(cfg, model):
157
141
  print(f'Configured models do not include "{model}". Use lmConfig to add it or pass a known alias.')
@@ -163,50 +147,28 @@ def main():
163
147
  assistant_reply = ""
164
148
  reply_ts = None
165
149
  first_chunk_seen = False
150
+ chunks = stream_chat(client, model, session["history"])
166
151
 
167
152
  try:
168
- stream = client.chat(model=model, messages=session["history"], stream=True)
169
- spinner.stop()
170
-
171
- spinner = StatusSpinner(WAITING_MSGS)
172
- spinner.start()
173
-
174
- for chunk in stream:
153
+ for content in chunks:
175
154
  if not first_chunk_seen:
176
155
  spinner.stop()
177
156
  first_chunk_seen = True
178
-
179
- # Standard Ollama client attribute access
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
157
+ print(content, end="", flush=True)
158
+ assistant_reply += content
187
159
  print()
188
160
  reply_ts = now_ts()
189
161
 
190
162
  except KeyboardInterrupt:
191
- try:
192
- if 'stream' in locals() and hasattr(stream, "close"):
193
- stream.close()
194
- except Exception:
195
- pass
163
+ chunks.close()
196
164
  print("\nInterrupted by user.")
197
165
  sys.exit(1)
198
166
 
199
167
  except Exception as e:
200
- msg = str(e)
201
- if "[Errno 111]" in msg or "Connection refused" in msg:
202
- print("\nOllama server did not respond: Connection refused.")
203
- print("Hint: Ollama may not be running, or it may not be listening on:", server_host)
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)
168
+ title, hint = resolve_ollama_error(e, server_host)
169
+ print("\n" + title)
170
+ if hint:
171
+ print(hint)
210
172
  sys.exit(1)
211
173
 
212
174
  finally:
File without changes