feat: sync with upstream — v1.2.0, in-app updates, Projects, modules
Brings the public tree back in line with the development repo after several weeks of drift caused by a stale publish include list. New: - In-app update path: GET /update/check compares the checkout against origin/main and POST /update/apply runs `ncp upgrade` detached (pull, rebuild, restart). The sidebar shows the version, checks on click, and offers an "update available" pill. - Projects: a project workspace groups chats and RAG documents, with per-project instructions and document retrieval scoped to the active project. Replaces the standalone Documents page. - modules/: auto-discovered feature plugins (mail, network) with their frontend counterparts and tests. - Memory curation runs in-process (synapse/memory/curator.py) on the chat model when a conversation goes idle. The separate memory service on :8001 is gone, along with the launcher lines that started it. Also: the KDE theme, panel and Promethean terminal assets, the full test suite, and VERSION 1.2.0. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -155,6 +155,66 @@ async def _remember(text: str = "", section: str = "General", **_) -> str:
|
||||
return json.dumps({"saved": text, "section": section or "General"})
|
||||
|
||||
|
||||
# --- Repo file access (read-only, scoped to PROJECT_ROOT) -------------------
|
||||
# Paths never leave the repo: every request is resolve()d and checked against
|
||||
# PROJECT_ROOT, which also kills symlink escapes. _DENIED covers the parts of
|
||||
# the tree that are either secrets, private data, or multi-GB noise.
|
||||
_DENIED = {
|
||||
".git", ".env", "Promethean", "node_modules", "models", "ollama",
|
||||
"runtime", "dist", "__pycache__", ".git-credentials",
|
||||
}
|
||||
_READ_MAX = 60_000
|
||||
|
||||
|
||||
def _repo_path(rel: str) -> "tuple[object, str | None]":
|
||||
"""Resolve a repo-relative path. Returns (path, error-string)."""
|
||||
from .nexus_config import PROJECT_ROOT
|
||||
rel = (rel or "").strip().lstrip("/")
|
||||
if not rel:
|
||||
return None, "path is required"
|
||||
target = (PROJECT_ROOT / rel).resolve()
|
||||
if not target.is_relative_to(PROJECT_ROOT):
|
||||
return None, "path escapes the project root"
|
||||
parts = set(target.relative_to(PROJECT_ROOT).parts)
|
||||
if parts & _DENIED or target.name.endswith((".db", ".db.sql", ".pem", ".key")):
|
||||
return None, f"{rel} is not readable"
|
||||
return target, None
|
||||
|
||||
|
||||
async def _read_file(path: str = "", **_) -> str:
|
||||
target, err = _repo_path(path)
|
||||
if err:
|
||||
return json.dumps({"error": err})
|
||||
if not target.is_file():
|
||||
return json.dumps({"error": f"{path} does not exist"})
|
||||
try:
|
||||
text = target.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as e:
|
||||
return json.dumps({"error": f"cannot read {path}: {e}"})
|
||||
return json.dumps({
|
||||
"path": path,
|
||||
"truncated": len(text) > _READ_MAX,
|
||||
"content": text[:_READ_MAX],
|
||||
})
|
||||
|
||||
|
||||
async def _list_files(pattern: str = "", **_) -> str:
|
||||
"""Glob the repo so the model discovers real paths instead of inventing them."""
|
||||
from .nexus_config import PROJECT_ROOT
|
||||
pattern = (pattern or "**/*.py").strip().lstrip("/")
|
||||
hits = []
|
||||
for f in PROJECT_ROOT.glob(pattern):
|
||||
if not f.is_file():
|
||||
continue
|
||||
target, err = _repo_path(str(f.relative_to(PROJECT_ROOT)))
|
||||
if err:
|
||||
continue
|
||||
hits.append(str(f.relative_to(PROJECT_ROOT)))
|
||||
if len(hits) >= 200:
|
||||
break
|
||||
return json.dumps(sorted(hits))
|
||||
|
||||
|
||||
# name -> (schema, callable). Schema is the OpenAI/Ollama function-tool format.
|
||||
REGISTRY: dict[str, tuple[dict, Callable[..., Awaitable[str]]]] = {
|
||||
"search_memory": (
|
||||
@@ -197,6 +257,35 @@ REGISTRY: dict[str, tuple[dict, Callable[..., Awaitable[str]]]] = {
|
||||
},
|
||||
_list_models,
|
||||
),
|
||||
"read_file": (
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a source file from the NexusOS repository. Path is relative to the project root, e.g. 'synapse/main.py'.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"path": {"type": "string", "description": "repo-relative file path"}},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
},
|
||||
_read_file,
|
||||
),
|
||||
"list_files": (
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_files",
|
||||
"description": "List files in the NexusOS repository matching a glob, e.g. 'synapse/**/*.py' or 'interface/web/src/*.jsx'. Use this to find real paths before reading.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"pattern": {"type": "string", "description": "glob relative to the project root"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
_list_files,
|
||||
),
|
||||
"search_documents": (
|
||||
{
|
||||
"type": "function",
|
||||
|
||||
Reference in New Issue
Block a user