Skip to content

Commit c7c855d

Browse files
RichmondAlakeclaude
andcommitted
feat(cli): add ollama to /login (Ollama Cloud sign-in)
/login now lists ollama. Because Ollama Cloud auth is a device flow (not a pasted key), `/login ollama` shells out to `ollama signin` rather than prompting for a key — local Ollama models still need no login. Pairs with the :cloud model support so a user can `/login ollama` then `/model glm-5.2:cloud`. Folded into 0.1.1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 80c98df commit c7c855d

3 files changed

Lines changed: 112 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
`curl -fsSL https://raw.githubusercontent.com/RichmondAlake/memorizz/main/install.sh | sh`
1717
(both bootstrap `uv`), alongside `pip install memorizz` / `uvx memorizz`.
1818
* **Ollama Cloud models.** An explicit `:cloud` model (e.g. `glm-5.2:cloud`) is
19-
now honored by the CLI instead of being substituted with a local model — run
20-
`ollama signin` once to authenticate.
19+
now honored by the CLI instead of being substituted with a local model.
20+
Authenticate with `/login ollama` (runs `ollama signin`), then
21+
`/model glm-5.2:cloud`.
2122

2223
### Fixes
2324

src/memorizz/cli/commands.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,12 +595,51 @@ def cmd_ui(session, args: str):
595595
("openai", "OPENAI_API_KEY", "OpenAI — LLM + embeddings"),
596596
("anthropic", "ANTHROPIC_API_KEY", "Anthropic — LLM"),
597597
("azure", "AZURE_OPENAI_API_KEY", "Azure OpenAI"),
598+
("ollama", "", "Ollama Cloud — sign in for ':cloud' models (e.g. glm-5.2:cloud)"),
598599
("tavily", "TAVILY_API_KEY", "Tavily — internet search"),
599600
("firecrawl", "FIRECRAWL_API_KEY", "Firecrawl — internet search"),
600601
("voyage", "VOYAGE_API_KEY", "Voyage AI — embeddings"),
601602
]
602603

603604

605+
def _login_ollama(console) -> None:
606+
"""Authenticate Ollama Cloud for ':cloud' models (e.g. glm-5.2:cloud).
607+
608+
Local Ollama models need no login. Cloud models are routed by the local
609+
daemon using credentials from ``ollama signin`` (a device/browser flow), so
610+
this shells out to it rather than storing a pasted key.
611+
"""
612+
import shutil
613+
import subprocess
614+
615+
console.print(
616+
"[dim]Local Ollama models need no login. This runs 'ollama signin' so "
617+
"you can use ':cloud' models (e.g. glm-5.2:cloud).[/dim]"
618+
)
619+
if shutil.which("ollama") is None:
620+
console.print(
621+
"[yellow]The 'ollama' CLI was not found.[/yellow] Install it from "
622+
"https://ollama.com/download, then run [cyan]ollama signin[/cyan]."
623+
)
624+
return
625+
console.print("Running [cyan]ollama signin[/cyan] …")
626+
try:
627+
result = subprocess.run(["ollama", "signin"])
628+
except Exception as exc: # pragma: no cover - environment dependent
629+
console.print(f"[yellow]Could not run 'ollama signin':[/yellow] {exc}")
630+
return
631+
if result.returncode == 0:
632+
console.print(
633+
"[green]Signed in to Ollama Cloud.[/green] Try "
634+
"[cyan]/model glm-5.2:cloud[/cyan]."
635+
)
636+
else:
637+
console.print(
638+
"[yellow]'ollama signin' didn't complete.[/yellow] You can run it "
639+
"directly in another terminal."
640+
)
641+
642+
604643
def cmd_login(session, args: str):
605644
console = _con(session)
606645
import getpass
@@ -612,7 +651,9 @@ def cmd_login(session, args: str):
612651
if not choice:
613652
console.print("[bold]Log in to a platform[/bold]")
614653
for i, (name, env_var, desc) in enumerate(_LOGIN_PROVIDERS, 1):
615-
status = " [green]✓ set[/green]" if os.environ.get(env_var) else ""
654+
status = (
655+
" [green]✓ set[/green]" if env_var and os.environ.get(env_var) else ""
656+
)
616657
console.print(f" [cyan]{i}[/cyan]. {name:<10} [dim]{desc}[/dim]{status}")
617658
try:
618659
choice = input("Select a platform [number or name]: ").strip().lower()
@@ -624,16 +665,25 @@ def cmd_login(session, args: str):
624665
return
625666

626667
# Resolve selection: number, known name, or an arbitrary KEY name.
668+
selected_name = None
627669
if choice.isdigit():
628670
idx = int(choice) - 1
629671
if not (0 <= idx < len(_LOGIN_PROVIDERS)):
630672
console.print(f"[yellow]Invalid selection:[/yellow] {choice}")
631673
return
674+
selected_name = _LOGIN_PROVIDERS[idx][0]
632675
key_name = _LOGIN_PROVIDERS[idx][1]
633676
elif choice in by_name:
677+
selected_name = choice
634678
key_name = by_name[choice][1]
635679
else:
636680
key_name = choice.upper()
681+
682+
# Ollama Cloud uses `ollama signin` (a device flow), not a pasted key.
683+
if selected_name == "ollama":
684+
_login_ollama(console)
685+
return
686+
637687
try:
638688
value = getpass.getpass(f"Paste {key_name} (hidden): ").strip()
639689
except (EOFError, KeyboardInterrupt):

tests/unit/test_cli_login.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright (c) 2024 Richmond Alake. All rights reserved.
2+
# Licensed under the PolyForm Noncommercial License 1.0.0.
3+
# See LICENSE file in the project root for full license information.
4+
5+
"""/login ollama -> Ollama Cloud sign-in (for ':cloud' models)."""
6+
7+
import shutil
8+
import subprocess
9+
import types
10+
11+
import pytest
12+
13+
from memorizz.cli import commands
14+
15+
16+
class _Console:
17+
def __init__(self):
18+
self.lines = []
19+
20+
def print(self, *a, **k):
21+
self.lines.append(" ".join(str(x) for x in a))
22+
23+
24+
@pytest.mark.unit
25+
def test_login_lists_ollama():
26+
assert "ollama" in [p[0] for p in commands._LOGIN_PROVIDERS]
27+
28+
29+
@pytest.mark.unit
30+
def test_login_ollama_runs_signin(monkeypatch):
31+
calls = {}
32+
monkeypatch.setattr(shutil, "which", lambda name: "/usr/local/bin/ollama")
33+
34+
class _Result:
35+
returncode = 0
36+
37+
def _run(cmd, *a, **k):
38+
calls["cmd"] = cmd
39+
return _Result()
40+
41+
monkeypatch.setattr(subprocess, "run", _run)
42+
session = types.SimpleNamespace(console=_Console())
43+
commands.cmd_login(session, "ollama")
44+
assert calls["cmd"] == ["ollama", "signin"]
45+
46+
47+
@pytest.mark.unit
48+
def test_login_ollama_missing_cli(monkeypatch):
49+
monkeypatch.setattr(shutil, "which", lambda name: None)
50+
51+
def _boom(*a, **k): # must NOT be called when the CLI is absent
52+
raise AssertionError("subprocess.run should not run without the ollama CLI")
53+
54+
monkeypatch.setattr(subprocess, "run", _boom)
55+
session = types.SimpleNamespace(console=_Console())
56+
commands.cmd_login(session, "ollama")
57+
out = "\n".join(session.console.lines).lower()
58+
assert "not found" in out and "ollama signin" in out

0 commit comments

Comments
 (0)