feat: add cortex_status and cortex_update tools
cortex_status: git branch/commit/ahead-behind + systemctl state — read-only cortex_update: git pull + syntax check all .py files + report; does NOT auto-restart. If syntax errors are found after pull, warns and blocks restart suggestion. Call cortex_restart separately to apply a clean update. Both are admin-only. cortex_update is confirm-required (modifies files on disk). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,7 +66,7 @@ The ⚡ toggle is **independent of the Role selector** — you can use any role
|
||||
| Web | `web_search`, `http_fetch` |
|
||||
| Files | `file_read` ¹, `file_list` ¹, `file_write` ¹ ² |
|
||||
| Shell | `shell_exec` ¹ ², `claude_allow_dir` ¹ |
|
||||
| System | `cortex_restart` ¹ ², `cortex_logs` ¹ |
|
||||
| System | `cortex_restart` ¹ ², `cortex_logs` ¹, `cortex_status` ¹, `cortex_update` ¹ ² |
|
||||
| Tasks | `task_list`, `task_create`, `task_update`, `task_complete` |
|
||||
| Cron | `cron_list`, `cron_add`, `cron_remove` ², `cron_toggle` |
|
||||
| Reminders | `reminders_add`, `reminders_list`, `reminders_remove`, `reminders_clear` ² |
|
||||
|
||||
@@ -48,7 +48,12 @@ from tools.scratch import (
|
||||
scratch_append as _scratch_append,
|
||||
scratch_clear as _scratch_clear,
|
||||
)
|
||||
from tools.system import cortex_restart as _cortex_restart, cortex_logs as _cortex_logs
|
||||
from tools.system import (
|
||||
cortex_restart as _cortex_restart,
|
||||
cortex_logs as _cortex_logs,
|
||||
cortex_status as _cortex_status,
|
||||
cortex_update as _cortex_update,
|
||||
)
|
||||
from tools.web import http_fetch as _http_fetch
|
||||
from tools.files import file_list as _file_list, file_write as _file_write
|
||||
from tools.notify import nc_talk_send as _nc_talk_send, email_send as _email_send
|
||||
@@ -389,6 +394,8 @@ _CALLABLES: dict[str, callable] = {
|
||||
"shell_exec": _shell_exec,
|
||||
"cortex_restart": _cortex_restart,
|
||||
"cortex_logs": _cortex_logs,
|
||||
"cortex_status": _cortex_status,
|
||||
"cortex_update": _cortex_update,
|
||||
"http_fetch": _http_fetch,
|
||||
"email_send": _email_send,
|
||||
"nc_talk_send": _nc_talk_send,
|
||||
@@ -793,6 +800,28 @@ _cortex_logs_declaration = types.FunctionDeclaration(
|
||||
),
|
||||
)
|
||||
|
||||
_cortex_status_declaration = types.FunctionDeclaration(
|
||||
name="cortex_status",
|
||||
description=(
|
||||
"Return Cortex service status: current git branch and commit, how many commits "
|
||||
"ahead/behind the remote, and the systemctl service state. "
|
||||
"Use to check what version is running or whether the service is healthy. "
|
||||
"ADMIN ONLY."
|
||||
),
|
||||
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||
)
|
||||
|
||||
_cortex_update_declaration = types.FunctionDeclaration(
|
||||
name="cortex_update",
|
||||
description=(
|
||||
"Pull the latest code from git, run a syntax check on all Python files, and report "
|
||||
"what changed. Does NOT restart automatically — call cortex_restart separately after "
|
||||
"reviewing the output. Will report syntax errors if the pull introduces broken code. "
|
||||
"ADMIN ONLY. Requires confirmation."
|
||||
),
|
||||
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||
)
|
||||
|
||||
_http_fetch_declaration = types.FunctionDeclaration(
|
||||
name="http_fetch",
|
||||
description=(
|
||||
@@ -919,6 +948,8 @@ TOOL_ROLES: dict[str, str] = {
|
||||
"claude_allow_dir": "admin",
|
||||
"cortex_restart": "admin",
|
||||
"cortex_logs": "admin",
|
||||
"cortex_status": "admin",
|
||||
"cortex_update": "admin",
|
||||
"file_read": "admin",
|
||||
"file_list": "admin",
|
||||
"file_write": "admin",
|
||||
@@ -932,6 +963,7 @@ TOOL_ROLES: dict[str, str] = {
|
||||
# the tool, prompting Claude to ask the user to confirm in a follow-up message.
|
||||
CONFIRM_REQUIRED: set[str] = {
|
||||
"cortex_restart",
|
||||
"cortex_update",
|
||||
"file_write",
|
||||
"shell_exec",
|
||||
"cron_remove",
|
||||
@@ -966,6 +998,8 @@ _ALL_DECLARATIONS: list[types.FunctionDeclaration] = [
|
||||
_shell_exec_declaration,
|
||||
_cortex_restart_declaration,
|
||||
_cortex_logs_declaration,
|
||||
_cortex_status_declaration,
|
||||
_cortex_update_declaration,
|
||||
_http_fetch_declaration,
|
||||
_email_send_declaration,
|
||||
_nc_talk_send_declaration,
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
System tools — local machine operations.
|
||||
|
||||
These tools affect the host system directly. Use with care.
|
||||
cortex_restart and cortex_logs require admin role.
|
||||
All tools in this module require the admin role.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Absolute paths — resolved relative to this file so they work regardless of cwd
|
||||
_CORTEX_DIR = Path(__file__).parent # .../Cortex_and_Inara_dev/cortex/
|
||||
_PROJECT_ROOT = _CORTEX_DIR.parent # .../Cortex_and_Inara_dev/
|
||||
|
||||
ALLOW_SCRIPT = "/home/scott/.local/bin/claude-allow-dir"
|
||||
|
||||
|
||||
@@ -124,3 +129,120 @@ async def cortex_logs(lines: int = 50) -> str:
|
||||
except Exception as e:
|
||||
logger.error("cortex_logs error: %s", e)
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
async def cortex_status() -> str:
|
||||
"""Return Cortex service status: git branch/commit, ahead/behind remote, and systemctl state."""
|
||||
lines = []
|
||||
|
||||
async def _git(*args: str) -> str:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"git", "-C", str(_PROJECT_ROOT), *args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)
|
||||
return stdout.decode(errors="replace").strip()
|
||||
|
||||
try:
|
||||
branch = await _git("rev-parse", "--abbrev-ref", "HEAD")
|
||||
commit = await _git("log", "--oneline", "-1")
|
||||
# fetch quietly so ahead/behind is current
|
||||
await asyncio.create_subprocess_exec(
|
||||
"git", "-C", str(_PROJECT_ROOT), "fetch", "--quiet",
|
||||
stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
ahead_behind = await _git("rev-list", "--left-right", "--count", f"HEAD...origin/{branch}")
|
||||
ahead, behind = (ahead_behind.split() + ["?", "?"])[:2]
|
||||
|
||||
lines.append(f"**Branch:** {branch} | ahead {ahead} / behind {behind}")
|
||||
lines.append(f"**Commit:** {commit}")
|
||||
except Exception as e:
|
||||
lines.append(f"Git info unavailable: {e}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"systemctl", "--user", "status", "cortex", "--no-pager", "-l",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)
|
||||
# First 15 lines of systemctl output is enough — avoids log flood
|
||||
status_lines = stdout.decode(errors="replace").splitlines()[:15]
|
||||
lines.extend(status_lines)
|
||||
except Exception as e:
|
||||
lines.append(f"systemctl status unavailable: {e}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def cortex_update() -> str:
|
||||
"""Pull the latest code from git, syntax-check all Python files, and report.
|
||||
|
||||
Does NOT restart automatically — call cortex_restart separately after reviewing
|
||||
the output if you want to apply changes.
|
||||
"""
|
||||
lines = []
|
||||
|
||||
async def _run(*cmd: str, cwd: Path = _PROJECT_ROOT, timeout: int = 30) -> tuple[int, str]:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd, cwd=str(cwd),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
||||
return proc.returncode, stdout.decode(errors="replace").strip()
|
||||
|
||||
# 1. Check for incoming commits before pulling
|
||||
try:
|
||||
await _run("git", "fetch", "--quiet")
|
||||
rc, incoming = await _run("git", "log", "--oneline", "HEAD..origin/HEAD")
|
||||
if rc == 0 and not incoming:
|
||||
# Double-check with branch name in case origin/HEAD isn't set
|
||||
branch_rc, branch = await _run("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
_, incoming = await _run("git", "log", "--oneline", f"HEAD..origin/{branch.strip()}")
|
||||
except asyncio.TimeoutError:
|
||||
return "Error: git fetch timed out — check network connectivity."
|
||||
except Exception as e:
|
||||
return f"Error during git fetch: {e}"
|
||||
|
||||
if not incoming:
|
||||
rc2, current = await _run("git", "log", "--oneline", "-1")
|
||||
return f"Already up to date.\n\nCurrent commit: {current}"
|
||||
|
||||
lines.append(f"**Incoming commits:**\n{incoming}\n")
|
||||
|
||||
# 2. Pull
|
||||
try:
|
||||
rc, pull_out = await _run("git", "pull", "--ff-only")
|
||||
except asyncio.TimeoutError:
|
||||
return "Error: git pull timed out."
|
||||
except Exception as e:
|
||||
return f"Error during git pull: {e}"
|
||||
|
||||
if rc != 0:
|
||||
return f"git pull failed (exit {rc}):\n{pull_out}"
|
||||
|
||||
lines.append(f"**git pull:**\n{pull_out}\n")
|
||||
|
||||
# 3. Syntax check all Python files under cortex/
|
||||
py_files = sorted(_CORTEX_DIR.rglob("*.py"))
|
||||
errors = []
|
||||
for f in py_files:
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "py_compile", str(f)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
errors.append(f" {f.relative_to(_PROJECT_ROOT)}: {result.stderr.strip()}")
|
||||
|
||||
if errors:
|
||||
lines.append(f"**Syntax errors — do NOT restart until fixed:**")
|
||||
lines.extend(errors)
|
||||
else:
|
||||
lines.append(f"**Syntax check:** {len(py_files)} files — all OK.")
|
||||
lines.append("Call `cortex_restart` to apply the update.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user