Skip to content

Commit 5728b9a

Browse files
thestack_aiclaude
andcommitted
fix: connect platform.py to context_engine.py for cross-platform support
- Replace hardcoded osascript/pmset/sysctl calls with platform.py collectors - Fix patcher force mode: use file-level merge instead of rmtree (prevents deleting original claude-code-telegram files) - Use asyncio.get_running_loop() instead of deprecated get_event_loop() Jarvis now works on Linux (psutil fallback) and degrades gracefully on Windows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5759c61 commit 5728b9a

File tree

2 files changed

+44
-72
lines changed

2 files changed

+44
-72
lines changed

mrstack/patcher.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,16 @@ def patch_install(site_pkg: Path | None = None, force: bool = False) -> bool:
5959
if not mod_src.is_dir():
6060
continue
6161
mod_dst = src_dir / mod
62-
if mod_dst.is_dir() and not force:
63-
# Merge: copy individual files, don't nuke the whole directory
64-
for f in mod_src.rglob("*"):
65-
if f.is_file():
66-
rel = f.relative_to(mod_src)
67-
dst = mod_dst / rel
68-
dst.parent.mkdir(parents=True, exist_ok=True)
69-
shutil.copy2(f, dst)
70-
else:
71-
if mod_dst.is_dir():
72-
shutil.rmtree(mod_dst)
73-
shutil.copytree(mod_src, mod_dst)
62+
# Always merge (file-level copy). Never rmtree — the target directory
63+
# contains original claude-code-telegram files we must not delete.
64+
for f in mod_src.rglob("*"):
65+
if f.is_file():
66+
rel = f.relative_to(mod_src)
67+
dst = mod_dst / rel
68+
if dst.is_file() and not force:
69+
continue # skip existing unless --force
70+
dst.parent.mkdir(parents=True, exist_ok=True)
71+
shutil.copy2(f, dst)
7472

7573
console.print("[green]Overlay modules installed.[/]")
7674

src/jarvis/context_engine.py

Lines changed: 34 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
"""Context engine — polls system state, detects context, evaluates triggers.
22
33
Mirrors ClipboardMonitor's asyncio.Task pattern:
4-
- 5-minute polling via subprocess calls (osascript, pmset, sysctl, git, etc.)
4+
- 5-minute polling via platform-abstracted collectors
55
- State classification (CODING, BROWSING, MEETING, ...)
66
- Rule-based trigger evaluation with cooldowns
77
- Publishes ScheduledEvent to EventBus on trigger fire
88
"""
99

1010
import asyncio
1111
import re
12-
import subprocess
1312
import time
1413
from collections import deque
1514
from typing import Any, Deque, Dict, List, Optional, Tuple
@@ -19,6 +18,14 @@
1918
from ..events.bus import EventBus
2019
from ..events.types import ScheduledEvent
2120
from .persona import ContextState, PersonaLayer
21+
from .platform import (
22+
FEATURES,
23+
collect_active_app,
24+
collect_battery,
25+
collect_chrome_tabs,
26+
collect_cpu_load,
27+
collect_git_info,
28+
)
2229

2330
logger = structlog.get_logger()
2431

@@ -238,71 +245,38 @@ async def _tick(self) -> None:
238245
logger.info("Jarvis trigger fired", trigger=trigger_id)
239246

240247
async def _collect_snapshot(self) -> ContextSnapshot:
241-
"""Collect system state via parallel subprocess calls."""
242-
loop = asyncio.get_event_loop()
248+
"""Collect system state via platform-abstracted collectors.
243249
244-
async def _run(cmd: List[str], timeout: int = 5) -> str:
245-
try:
246-
result = await loop.run_in_executor(
247-
None,
248-
lambda: subprocess.run(
249-
cmd, capture_output=True, text=True, timeout=timeout
250-
),
251-
)
252-
return result.stdout.strip() if result.returncode == 0 else ""
253-
except Exception:
254-
return ""
250+
Uses platform.py for cross-platform support:
251+
- macOS: osascript, pmset, sysctl (native)
252+
- Linux: xdotool, psutil (fallback)
253+
- Windows: psutil only (Jarvis limited)
254+
"""
255+
loop = asyncio.get_running_loop()
256+
257+
# Run all collectors in parallel via executor (they do subprocess calls)
258+
active_app_fut = loop.run_in_executor(None, collect_active_app)
259+
battery_fut = loop.run_in_executor(None, collect_battery)
260+
cpu_fut = loop.run_in_executor(None, collect_cpu_load)
261+
git_fut = loop.run_in_executor(
262+
None, collect_git_info, self.working_directory
263+
)
264+
chrome_fut = loop.run_in_executor(None, collect_chrome_tabs)
255265

256-
# Run all collectors in parallel
257266
results = await asyncio.gather(
258-
_run([
259-
"osascript", "-e",
260-
'tell app "System Events" to get name of first process '
261-
"whose frontmost is true",
262-
]),
263-
_run(["pmset", "-g", "batt"]),
264-
_run(["sysctl", "-n", "vm.loadavg"]),
265-
_run([
266-
"git", "-C", self.working_directory,
267-
"branch", "--show-current",
268-
]),
269-
_run([
270-
"git", "-C", self.working_directory,
271-
"status", "--short",
272-
]),
273-
_run([
274-
"osascript", "-e",
275-
'tell application "Google Chrome" to get title of active tab '
276-
"of front window",
277-
]),
267+
active_app_fut, battery_fut, cpu_fut, git_fut, chrome_fut,
278268
return_exceptions=True,
279269
)
280270

281-
# Parse results
282271
active_app = results[0] if isinstance(results[0], str) else ""
283-
284-
battery_pct = 100
285-
battery_charging = True
286-
batt_str = results[1] if isinstance(results[1], str) else ""
287-
batt_match = re.search(r"(\d+)%", batt_str)
288-
if batt_match:
289-
battery_pct = int(batt_match.group(1))
290-
battery_charging = "charging" in batt_str.lower() or "charged" in batt_str.lower()
291-
292-
cpu_load = 0.0
293-
load_str = results[2] if isinstance(results[2], str) else ""
294-
load_match = re.search(r"[\d.]+", load_str)
295-
if load_match:
296-
try:
297-
cpu_load = float(load_match.group())
298-
except ValueError:
299-
pass
300-
301-
git_branch = results[3] if isinstance(results[3], str) else ""
302-
git_dirty = bool(results[4]) if isinstance(results[4], str) else False
303-
304-
chrome_tab = results[5] if isinstance(results[5], str) else ""
305-
chrome_tabs = [chrome_tab] if chrome_tab else []
272+
battery_pct, battery_charging = (
273+
results[1] if isinstance(results[1], tuple) else (100, True)
274+
)
275+
cpu_load = results[2] if isinstance(results[2], float) else 0.0
276+
git_branch, git_dirty = (
277+
results[3] if isinstance(results[3], tuple) else ("", False)
278+
)
279+
chrome_tabs = results[4] if isinstance(results[4], list) else []
306280

307281
return ContextSnapshot(
308282
active_app=active_app,

0 commit comments

Comments
 (0)