Clean up lint/LSP across gateway, Android, and desktop (alpha -> stable)
Gateway (gateway-plugin/): - Fix interactive_setup broken imports: print helpers were imported from the wrong hermes module (hermes_cli.config instead of hermes_cli.cli_output) plus a non-existent print_code; the try/except swallowed the ImportError so `hermes gateway setup` for android always bailed out early. - Fix release_scoped_lock type error (str | None passed where str required). - Rewrite empty `except: pass` blocks as contextlib.suppress with rationale. - Restructure two ambiguous ws_server try blocks (hello-auth, frame loop). - Ruff cleanup: type annotations, import sorting, line wrapping, magic values -> named constants, `raise ... from e`, complexity. Add gateway-plugin/ruff.toml. - Add pyrightconfig.json so the Python LSP resolves hermes-runtime imports. - Suppress verified false positives inline (parameterized SQL, column-name "secrets", hermes-generated media path). Android (app/androidApp + app/shared): - Consolidate launcher icons into a single mipmap-anydpi (minSdk 29 >= 26) with the monochrome layer; clears ObsoleteSdkInt + MonochromeLauncherIcon. - Bump core-splashscreen 1.0.1 -> 1.2.0; pin targetSdk 34 (deliberate). - Suppress verified findings inline (LAN ws:// default, correct GCM IV usage). Desktop (app/desktopApp): - Move the desktop to a Java 21 runtime (org.gradle.java.home) and set the desktop jvmTarget to 21 (Android stays JVM 17 / minSdk 29). Fixes the startup UnsupportedClassVersionError and restores Markdown renderer 0.44.0. Tooling/config: - .pi-lens.json: disable verified-noisy heuristics (documented in docs). - .gitleaks.toml: allowlist git-ignored false-positive paths. - docs/18-code-review.md: full findings + verification. Verified: ruff clean, pyright 0 errors, 64/64 gateway tests, all Kotlin tests, Android lint 0 issues, Android installed+launched on device, desktop launches on JDK 21.
This commit is contained in:
1 parent
9f3f9842c8
commit
678c0344c8
27 files changed
+928
-454
No files matched your search
+53
-51
@@ -20,12 +20,14 @@ Storage: ``get_hermes_home()/"android"/channels.db``.
|
||||
Milestone M3.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import contextlib
|
||||
import logging
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -73,9 +75,7 @@ class ChannelDirectory:
|
||||
"""
|
||||
)
|
||||
# Migrate existing DBs: add the cosmetic columns if missing.
|
||||
existing = {
|
||||
row[1] for row in self._conn.execute("PRAGMA table_info(channels)")
|
||||
}
|
||||
existing = {row[1] for row in self._conn.execute("PRAGMA table_info(channels)")}
|
||||
if "favorite" not in existing:
|
||||
self._conn.execute(
|
||||
"ALTER TABLE channels ADD COLUMN favorite INTEGER NOT NULL DEFAULT 0"
|
||||
@@ -120,7 +120,7 @@ class ChannelDirectory:
|
||||
|
||||
# ── default channel ───────────────────────────────────────────────────
|
||||
|
||||
def ensure_default(self, chat_id: str, name: str) -> Dict[str, Any]:
|
||||
def ensure_default(self, chat_id: str, name: str) -> dict[str, Any]:
|
||||
"""Ensure the default (home) channel exists. Idempotent.
|
||||
|
||||
If a row already exists for *chat_id* it is kept (name refreshed only
|
||||
@@ -152,16 +152,18 @@ class ChannelDirectory:
|
||||
(KIND_DEFAULT, chat_id),
|
||||
)
|
||||
# Exactly one default: clear any other default flag.
|
||||
self._conn.execute(
|
||||
"UPDATE channels SET is_default = 0 WHERE chat_id != ?", (chat_id,)
|
||||
)
|
||||
self._conn.execute("UPDATE channels SET is_default = 0 WHERE chat_id != ?", (chat_id,))
|
||||
self._conn.commit()
|
||||
entry = self.get(chat_id)
|
||||
if entry is not None:
|
||||
return entry
|
||||
return {
|
||||
"chat_id": chat_id, "name": name, "kind": KIND_DEFAULT,
|
||||
"parent_chat_id": None, "is_default": True, "archived": False,
|
||||
"chat_id": chat_id,
|
||||
"name": name,
|
||||
"kind": KIND_DEFAULT,
|
||||
"parent_chat_id": None,
|
||||
"is_default": True,
|
||||
"archived": False,
|
||||
"created": time.time(),
|
||||
}
|
||||
|
||||
@@ -171,8 +173,8 @@ class ChannelDirectory:
|
||||
self,
|
||||
name: str,
|
||||
kind: str = KIND_CHANNEL,
|
||||
parent_chat_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
parent_chat_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Mint a new channel (or thread) and store it. Returns the entry."""
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
@@ -194,12 +196,16 @@ class ChannelDirectory:
|
||||
if entry is not None:
|
||||
return entry
|
||||
return {
|
||||
"chat_id": chat_id, "name": name, "kind": kind,
|
||||
"parent_chat_id": parent_chat_id, "is_default": False,
|
||||
"archived": False, "created": now,
|
||||
"chat_id": chat_id,
|
||||
"name": name,
|
||||
"kind": kind,
|
||||
"parent_chat_id": parent_chat_id,
|
||||
"is_default": False,
|
||||
"archived": False,
|
||||
"created": now,
|
||||
}
|
||||
|
||||
def rename(self, chat_id: str, name: str) -> Optional[Dict[str, Any]]:
|
||||
def rename(self, chat_id: str, name: str) -> dict[str, Any] | None:
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
raise ValueError("channel name required")
|
||||
@@ -213,7 +219,7 @@ class ChannelDirectory:
|
||||
return None
|
||||
return self.get(chat_id)
|
||||
|
||||
def set_default(self, chat_id: str) -> Optional[Dict[str, Any]]:
|
||||
def set_default(self, chat_id: str) -> dict[str, Any] | None:
|
||||
"""Mark *chat_id* as the default channel (clears the previous one).
|
||||
|
||||
The default channel is the user's chat surface, so the automation
|
||||
@@ -228,14 +234,13 @@ class ChannelDirectory:
|
||||
return None
|
||||
self._conn.execute("UPDATE channels SET is_default = 0")
|
||||
self._conn.execute(
|
||||
"UPDATE channels SET is_default = 1, automation = 0 "
|
||||
"WHERE chat_id = ?",
|
||||
"UPDATE channels SET is_default = 1, automation = 0 WHERE chat_id = ?",
|
||||
(chat_id,),
|
||||
)
|
||||
self._conn.commit()
|
||||
return self.get(chat_id)
|
||||
|
||||
def set_favorite(self, chat_id: str, on: bool) -> Optional[Dict[str, Any]]:
|
||||
def set_favorite(self, chat_id: str, on: bool) -> dict[str, Any] | None:
|
||||
"""Toggle the cosmetic favorite flag (sorts to the top of the list)."""
|
||||
with self._lock:
|
||||
cur = self._conn.execute(
|
||||
@@ -247,7 +252,7 @@ class ChannelDirectory:
|
||||
return None
|
||||
return self.get(chat_id)
|
||||
|
||||
def set_icon(self, chat_id: str, icon: Optional[str], color: Optional[str]) -> Optional[Dict[str, Any]]:
|
||||
def set_icon(self, chat_id: str, icon: str | None, color: str | None) -> dict[str, Any] | None:
|
||||
"""Set the channel's cosmetic icon (base64 image) and/or avatar color.
|
||||
|
||||
``icon`` is a base64-encoded image (or ``None`` to clear it); ``color``
|
||||
@@ -264,7 +269,7 @@ class ChannelDirectory:
|
||||
return None
|
||||
return self.get(chat_id)
|
||||
|
||||
def set_automation(self, chat_id: str, on: bool) -> Optional[Dict[str, Any]]:
|
||||
def set_automation(self, chat_id: str, on: bool) -> dict[str, Any] | None:
|
||||
"""Mark *chat_id* as an automation channel (or clear the flag).
|
||||
|
||||
Automation channels are read-only for the user: they only receive
|
||||
@@ -287,7 +292,7 @@ class ChannelDirectory:
|
||||
self._conn.commit()
|
||||
return self.get(chat_id)
|
||||
|
||||
def delete(self, chat_id: str) -> Optional[Dict[str, Any]]:
|
||||
def delete(self, chat_id: str) -> dict[str, Any] | None:
|
||||
"""Soft-delete (archive) a channel. History stays for search.
|
||||
|
||||
The default channel cannot be deleted. Returns the (archived) entry,
|
||||
@@ -299,39 +304,40 @@ class ChannelDirectory:
|
||||
).fetchone()
|
||||
if row is None or row["is_default"]:
|
||||
return None
|
||||
self._conn.execute(
|
||||
"UPDATE channels SET archived = 1 WHERE chat_id = ?", (chat_id,)
|
||||
)
|
||||
self._conn.execute("UPDATE channels SET archived = 1 WHERE chat_id = ?", (chat_id,))
|
||||
self._conn.commit()
|
||||
return self.get(chat_id)
|
||||
|
||||
# ── reads ─────────────────────────────────────────────────────────────
|
||||
|
||||
def get(self, chat_id: str) -> Optional[Dict[str, Any]]:
|
||||
def get(self, chat_id: str) -> dict[str, Any] | None:
|
||||
with self._lock:
|
||||
row = self._conn.execute(
|
||||
"SELECT * FROM channels WHERE chat_id = ?", (chat_id,)
|
||||
).fetchone()
|
||||
return _row_to_entry(row) if row else None
|
||||
|
||||
def list(self, include_archived: bool = False) -> List[Dict[str, Any]]:
|
||||
def list(self, include_archived: bool = False) -> list[dict[str, Any]]:
|
||||
"""Directory listing. Default first, then favorites, then creation order."""
|
||||
sql = "SELECT * FROM channels"
|
||||
if not include_archived:
|
||||
sql += " WHERE archived = 0"
|
||||
sql += " ORDER BY is_default DESC, favorite DESC, created ASC"
|
||||
with self._lock:
|
||||
# Safe: fully static SQL (no user data); the variable is only to
|
||||
# toggle the optional archived filter.
|
||||
# pi-lens-ignore: python-sql-injection
|
||||
rows = self._conn.execute(sql).fetchall()
|
||||
return [_row_to_entry(r) for r in rows]
|
||||
|
||||
def default(self) -> Optional[Dict[str, Any]]:
|
||||
def default(self) -> dict[str, Any] | None:
|
||||
with self._lock:
|
||||
row = self._conn.execute(
|
||||
"SELECT * FROM channels WHERE is_default = 1 LIMIT 1"
|
||||
).fetchone()
|
||||
return _row_to_entry(row) if row else None
|
||||
|
||||
def threads_for(self, chat_id: str) -> List[Dict[str, Any]]:
|
||||
def threads_for(self, chat_id: str) -> builtins.list[dict[str, Any]]:
|
||||
"""All (non-archived) threads under *chat_id*, oldest first."""
|
||||
with self._lock:
|
||||
rows = self._conn.execute(
|
||||
@@ -341,7 +347,7 @@ class ChannelDirectory:
|
||||
).fetchall()
|
||||
return [_row_to_entry(r) for r in rows]
|
||||
|
||||
def resolve_entry(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
def resolve_entry(self, name: str) -> dict[str, Any] | None:
|
||||
"""Resolve a friendly name to a directory entry (case-insensitive).
|
||||
|
||||
Matches non-archived channels/threads by exact name first, then by
|
||||
@@ -352,23 +358,19 @@ class ChannelDirectory:
|
||||
if not query:
|
||||
return None
|
||||
with self._lock:
|
||||
rows = self._conn.execute(
|
||||
"SELECT * FROM channels WHERE archived = 0"
|
||||
).fetchall()
|
||||
rows = self._conn.execute("SELECT * FROM channels WHERE archived = 0").fetchall()
|
||||
entries = [_row_to_entry(r) for r in rows]
|
||||
exact = [e for e in entries if (e["name"] or "").strip().lower() == query]
|
||||
if len(exact) == 1:
|
||||
return exact[0]
|
||||
if len(exact) > 1:
|
||||
return None
|
||||
prefix = [
|
||||
e for e in entries if (e["name"] or "").strip().lower().startswith(query)
|
||||
]
|
||||
prefix = [e for e in entries if (e["name"] or "").strip().lower().startswith(query)]
|
||||
if len(prefix) == 1:
|
||||
return prefix[0]
|
||||
return None
|
||||
|
||||
def resolve_name(self, name: str) -> Optional[str]:
|
||||
def resolve_name(self, name: str) -> str | None:
|
||||
"""Resolve a friendly name to a valid chat_id (case-insensitive).
|
||||
|
||||
For a thread, returns the *parent* chat_id (the thread's session lane
|
||||
@@ -383,14 +385,12 @@ class ChannelDirectory:
|
||||
return entry["chat_id"]
|
||||
|
||||
def close(self) -> None:
|
||||
with self._lock:
|
||||
try:
|
||||
self._conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
with self._lock, contextlib.suppress(Exception):
|
||||
# Best-effort: a close failure on shutdown is not actionable.
|
||||
self._conn.close()
|
||||
|
||||
|
||||
def _row_to_entry(row: sqlite3.Row) -> Dict[str, Any]:
|
||||
def _row_to_entry(row: sqlite3.Row) -> dict[str, Any]:
|
||||
return {
|
||||
"chat_id": row["chat_id"],
|
||||
"name": row["name"],
|
||||
@@ -415,24 +415,26 @@ def _row_to_entry(row: sqlite3.Row) -> Dict[str, Any]:
|
||||
# Keyed on ``get_hermes_home()`` so a profile switch rebuilds it.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_directory: Optional[ChannelDirectory] = None
|
||||
_directory_home: Optional[Path] = None
|
||||
_directory: ChannelDirectory | None = None
|
||||
_directory_home: Path | None = None
|
||||
_directory_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_directory() -> ChannelDirectory:
|
||||
"""Return the process-wide channel directory for the active profile."""
|
||||
global _directory, _directory_home
|
||||
# Module-level singleton keyed on the active profile; the global is the
|
||||
# intended pattern here (see the block comment above).
|
||||
global _directory, _directory_home # noqa: PLW0603
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
home = Path(get_hermes_home())
|
||||
with _directory_lock:
|
||||
if _directory is None or _directory_home != home:
|
||||
if _directory is not None:
|
||||
try:
|
||||
# Best-effort: the old directory is being replaced; a close
|
||||
# failure is not actionable.
|
||||
with contextlib.suppress(Exception):
|
||||
_directory.close()
|
||||
except Exception:
|
||||
pass
|
||||
_directory = ChannelDirectory(home / "android" / "channels.db")
|
||||
_directory_home = home
|
||||
return _directory
|
||||
return _directory
|
||||
Reference in new issue
Block a user