Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/stack/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,7 @@ def missing_secrets(stacklet_id):

checkout = Checkout(stck.root)
position = checkout.describe() if checkout.is_git() else ""
latest = latest_tag(checkout.tags()) if position else ""
latest = (latest_tag(checkout.tags()) or "") if position else ""
release = doctor.check_release(
position, latest, up_to_date=bool(latest) and checkout.contains(latest))
if release:
Expand Down
13 changes: 7 additions & 6 deletions lib/stack/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import ssl
import subprocess
import time
import urllib.error
import urllib.request
from pathlib import Path

Expand Down Expand Up @@ -50,7 +51,7 @@ def compose(compose_file: str | Path, *args) -> tuple[int, str, str]:
return result.returncode, result.stdout, result.stderr


def compose_up(compose_file: str | Path, env: dict = None) -> tuple[int, str]:
def compose_up(compose_file: str | Path, env: dict | None = None) -> tuple[int, str]:
"""Start containers, always force-recreating. Returns (exit_code, error_output).

`--force-recreate` is unconditional because compose's service-config
Expand Down Expand Up @@ -90,7 +91,7 @@ def compose_down(compose_file: str | Path) -> tuple[int, str]:
return code, (stdout + stderr).strip()


def compose_pull(compose_file: str | Path, env: dict = None) -> None:
def compose_pull(compose_file: str | Path, env: dict | None = None) -> None:
"""Pull images for a compose file. Streams output."""
full_env = {**__import__("os").environ, **(env or {})}
_docker(
Expand All @@ -99,7 +100,7 @@ def compose_pull(compose_file: str | Path, env: dict = None) -> None:
)


def compose_build(compose_file: str | Path, env: dict = None) -> None:
def compose_build(compose_file: str | Path, env: dict | None = None) -> None:
"""Build images for a compose file. Streams output."""
full_env = {**__import__("os").environ, **(env or {})}
_docker(
Expand Down Expand Up @@ -206,7 +207,7 @@ def check_docker() -> tuple[str | None, str | None]:
return None, "Docker is not installed"


def check_health(url: str, headers: dict = None) -> bool:
def check_health(url: str, headers: dict | None = None) -> bool:
"""Quick probe — returns True if URL responds with 2xx."""
try:
req = urllib.request.Request(url, headers=headers or {})
Expand All @@ -216,7 +217,7 @@ def check_health(url: str, headers: dict = None) -> bool:
return False


def probe_health(url: str, headers: dict = None, timeout: float = 3) -> str:
def probe_health(url: str, headers: dict | None = None, timeout: float = 3) -> str:
"""Single-shot health probe. Returns 'ready', 'auth', or 'down'.

No retries — the caller loops if they want a wait. Used by both
Expand All @@ -236,7 +237,7 @@ def probe_health(url: str, headers: dict = None, timeout: float = 3) -> str:


def wait_for_health(url: str, timeout: int = 120, interval: int = 3,
headers: dict = None) -> str:
headers: dict | None = None) -> str:
"""Poll a URL until it responds. Returns 'ready', 'auth', or 'timeout'.

Used after compose up to wait for a service to actually be ready,
Expand Down
3 changes: 2 additions & 1 deletion lib/stack/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,11 @@ def fail(self, hint=None):
class _TerminalSpinner:
"""Animated braille spinner. Use as context manager."""

_thread: threading.Thread # started by __enter__

def __init__(self, msg):
self.msg = msg
self._running = False
self._thread = None
self._ok = True
self._hint = None

Expand Down
3 changes: 2 additions & 1 deletion lib/stack/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,11 @@ def banner(product, subtitle=""):
class Spinner:
"""Minimal braille spinner for long operations."""

thread: threading.Thread # started by __enter__

def __init__(self, msg):
self.msg = msg
self.running = False
self.thread = None
self.ok = True

def __enter__(self):
Expand Down
4 changes: 2 additions & 2 deletions lib/stack/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ def _build_template_vars(self) -> dict:
"shared_bucket": self._cfg("core", "shared_bucket", "family"),
# Wiki freshness (the memory stacklet's curator sidecar).
# Bool/int from stack.toml render as env-friendly strings.
"wiki_auto_rebuild": str(self._cfg("memory", "wiki_auto_rebuild", True)).lower(),
"wiki_rebuild_quiet_secs": str(self._cfg("memory", "wiki_rebuild_quiet_secs", 180)),
"wiki_auto_rebuild": self._cfg("memory", "wiki_auto_rebuild", "true").lower(),
"wiki_rebuild_quiet_secs": self._cfg("memory", "wiki_rebuild_quiet_secs", "180"),
"wiki_nightly": self._cfg("memory", "wiki_nightly", "03:30"),
# AI service URLs — host-side for CLI, docker-side for containers
"ai_openai_url": ai_openai_url,
Expand Down
2 changes: 1 addition & 1 deletion stacklets/ai/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def _render_progress(percent: float, downloaded: int, total: int):
end="", flush=True)


def _format_size(size: int) -> str:
def _format_size(size: float) -> str:
"""Format bytes as human-readable size."""
for unit in ("B", "KB", "MB", "GB", "TB"):
if size < 1024:
Expand Down
9 changes: 5 additions & 4 deletions stacklets/ai/omlx.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import http.cookiejar
import json
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass

Expand Down Expand Up @@ -91,7 +92,7 @@ def list_downloaded(self) -> list[dict]:

def get_model_info(self, repo_id: str) -> ModelInfo | None:
"""Get model info from HuggingFace. Returns None if not found."""
encoded = urllib.request.quote(repo_id, safe="")
encoded = urllib.parse.quote(repo_id, safe="")
result = self._request("GET", f"/admin/api/hf/model-info?repo_id={encoded}")
if result is None or "error" in result:
return None
Expand Down Expand Up @@ -148,21 +149,21 @@ def load_model(self, model_id: str) -> bool:
model_id is the folder name (e.g. "Qwen3.5-9B-MLX-8bit"),
not the full repo_id.
"""
encoded = urllib.request.quote(model_id, safe="")
encoded = urllib.parse.quote(model_id, safe="")
result = self._request("POST", f"/admin/api/models/{encoded}/load")
return result is not None and result.get("success", False)

def get_model_settings(self, model_id: str) -> dict | None:
"""Get per-model settings. Returns the settings dict or None."""
encoded = urllib.request.quote(model_id, safe="")
encoded = urllib.parse.quote(model_id, safe="")
result = self._request("PUT", f"/admin/api/models/{encoded}/settings", {})
if result is None:
return None
return result.get("settings", {})

def update_model_settings(self, model_id: str, **settings) -> bool:
"""Update per-model settings (chat_template_kwargs, context size, etc.)."""
encoded = urllib.request.quote(model_id, safe="")
encoded = urllib.parse.quote(model_id, safe="")
result = self._request("PUT", f"/admin/api/models/{encoded}/settings", settings)
return result is not None and result.get("success", False)

Expand Down
7 changes: 5 additions & 2 deletions stacklets/ai/thinking.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def is_qwen3(model_id: str) -> bool:
return "qwen3" in lower or "qwen-3" in lower


def _quiet(msg: str) -> None:
return None


def disable_thinking(client, model_id: str, log=None) -> bool:
"""Disable thinking for a Qwen3-family model via oMLX admin API.

Expand All @@ -29,8 +33,7 @@ def disable_thinking(client, model_id: str, log=None) -> bool:
Returns True if thinking is now disabled, False on failure.
"""
if log is None:
def log(msg):
return None
log = _quiet

if not is_qwen3(model_id):
log(f"Not a Qwen3 model ({model_id}), skipping")
Expand Down
5 changes: 3 additions & 2 deletions stacklets/core/bot-runner/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import json
import ssl
import urllib.error
import urllib.parse
import urllib.request

from loguru import logger
Expand Down Expand Up @@ -52,7 +53,7 @@ def _admin_login(base, admin_user, admin_password):
def _resolve_space(base, token, server_name):
"""Resolve the Family Space room ID, or None."""
space_alias = f"#family:{server_name}"
encoded_alias = urllib.request.quote(space_alias)
encoded_alias = urllib.parse.quote(space_alias)
status, resp = _api("GET", f"{base}/_matrix/client/v3/directory/room/{encoded_alias}", token=token)
return resp.get("room_id") if status == 200 else None

Expand Down Expand Up @@ -133,7 +134,7 @@ def ensure_rooms(bots, homeserver, server_name, admin_user, admin_password,
def _ensure_room(base, token, server_name, alias, topic, space_id):
"""Create a room or resolve an existing one."""
full_alias = f"#{alias}:{server_name}"
encoded = urllib.request.quote(full_alias)
encoded = urllib.parse.quote(full_alias)

# Check if room exists
status, resp = _api("GET", f"{base}/_matrix/client/v3/directory/room/{encoded}", token=token)
Expand Down
2 changes: 1 addition & 1 deletion stacklets/core/bot-runner/microbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ async def _room_send(
room_id=room_id, message_type=message_type, content=content,
)

async def _react(self, room_id: str, event_id: str, emoji: str) -> None:
async def _react(self, room_id: str, event_id: str | None, emoji: str) -> None:
"""Annotate an event with an emoji reaction (MSC2677).

The bot's way to signal state on a specific message without a
Expand Down
Loading
Loading