Add files via upload
This commit is contained in:
1 parent
d178f6b9ee
commit
2c40eb55ed
11 files changed
+1447
-237
No files matched your search
+196
-29
@@ -8,6 +8,7 @@ import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -344,6 +345,102 @@ def _detect_default_browser() -> str:
|
||||
return "chrome"
|
||||
|
||||
|
||||
_SEARCH_ENGINES: dict[str, str] = {
|
||||
"google": "https://www.google.com/search?q=",
|
||||
"bing": "https://www.bing.com/search?q=",
|
||||
"duckduckgo": "https://duckduckgo.com/?q=",
|
||||
"yandex": "https://yandex.com/search/?text=",
|
||||
}
|
||||
|
||||
_MAC_APP_NAMES: dict[str, str] = {
|
||||
"chrome": "Google Chrome",
|
||||
"edge": "Microsoft Edge",
|
||||
"firefox": "Firefox",
|
||||
"opera": "Opera",
|
||||
"operagx": "Opera GX",
|
||||
"brave": "Brave Browser",
|
||||
"vivaldi": "Vivaldi",
|
||||
"safari": "Safari",
|
||||
}
|
||||
|
||||
# Windows registry lookup names for browsers whose spec has no explicit binary
|
||||
_WIN_EXE_HINTS: dict[str, str] = {"chrome": "chrome", "edge": "msedge"}
|
||||
|
||||
|
||||
def _open_native(url: str, browser_name: Optional[str]) -> str:
|
||||
"""
|
||||
Kullanıcının GERÇEK tarayıcısını normal şekilde açar — kendi profili,
|
||||
giriş yapılmış hesapları ve eklentileriyle. Otomasyon bağlanmaz, bu yüzden
|
||||
about:blank sekmesi veya boş profil ASLA görünmez.
|
||||
url boş ise tarayıcı URL'siz başlatılır (kendi açılış sayfası /
|
||||
oturum geri yükleme ile) — tıpkı kullanıcının kendisi açmış gibi.
|
||||
Windows / macOS / Linux üçünde de çalışır.
|
||||
"""
|
||||
url = _normalize_url(url) if url and url.strip() else ""
|
||||
if url == "about:blank":
|
||||
url = ""
|
||||
|
||||
name = None
|
||||
if browser_name:
|
||||
name = _ALIASES.get(browser_name.lower().strip(), browser_name.lower().strip())
|
||||
elif not url:
|
||||
# URL yok → sadece pencere açılacak; varsayılan tarayıcının exe'si gerekir
|
||||
name = _detect_default_browser()
|
||||
|
||||
# Specific browser → launch its own executable, exactly like the user would.
|
||||
if name:
|
||||
if _OS == "Darwin":
|
||||
app = _MAC_APP_NAMES.get(name)
|
||||
if app:
|
||||
cmd = ["open", "-a", app] + ([url] if url else [])
|
||||
try:
|
||||
subprocess.run(cmd, check=True, timeout=10)
|
||||
return f"Opened in {name}: {url}" if url else f"Opened {name}."
|
||||
except Exception as e:
|
||||
print(f"[Browser] 'open -a {app}' failed ({e}), trying binary…")
|
||||
|
||||
spec = _resolve_browser(name)
|
||||
exe = spec.get("exe") if spec else None
|
||||
if not exe and _OS == "Windows":
|
||||
if name in ("opera", "operagx"):
|
||||
exe = _find_opera_windows()
|
||||
else:
|
||||
exe = _find_exe_windows(_WIN_EXE_HINTS.get(name, name))
|
||||
if exe:
|
||||
try:
|
||||
subprocess.Popen(
|
||||
[exe, url] if url else [exe],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return f"Opened in {name}: {url}" if url else f"Opened {name}."
|
||||
except Exception as e:
|
||||
print(f"[Browser] Native launch failed for {name}: {e}")
|
||||
print(f"[Browser] '{name}' not found — falling back to default browser.")
|
||||
|
||||
if not url:
|
||||
return "Could not find a browser to open."
|
||||
|
||||
# Default browser via the OS — exactly like the user clicking a link.
|
||||
try:
|
||||
if _OS == "Windows":
|
||||
os.startfile(url) # ShellExecute → default browser
|
||||
elif _OS == "Darwin":
|
||||
subprocess.run(["open", url], check=True, timeout=10)
|
||||
else:
|
||||
subprocess.Popen(
|
||||
["xdg-open", url],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return f"Opened in your default browser: {url}"
|
||||
except Exception:
|
||||
try:
|
||||
if webbrowser.open(url):
|
||||
return f"Opened in your default browser: {url}"
|
||||
except Exception:
|
||||
pass
|
||||
return f"Could not open a browser for: {url}"
|
||||
|
||||
|
||||
class _BrowserSession:
|
||||
"""
|
||||
Bir tarayıcı örneği için tam oturum.
|
||||
@@ -406,6 +503,16 @@ class _BrowserSession:
|
||||
pass
|
||||
self._context = self._page = None
|
||||
|
||||
async def _adopt_page(self) -> Page:
|
||||
"""
|
||||
launch_persistent_context zaten bir başlangıç sekmesi açar.
|
||||
Yeni bir boş sekme (about:blank) açmak yerine o sekmeyi devralır —
|
||||
böylece kullanıcı fazladan boş sekme görmez.
|
||||
"""
|
||||
await asyncio.sleep(0.3)
|
||||
pages = self._context.pages
|
||||
return pages[0] if pages else await self._context.new_page()
|
||||
|
||||
async def _launch(self):
|
||||
"""
|
||||
Tarayıcıyı gerçek kullanıcı profiliyle başlatır.
|
||||
@@ -433,6 +540,7 @@ class _BrowserSession:
|
||||
"slow_mo": 0,
|
||||
"viewport": None,
|
||||
"no_viewport": True,
|
||||
"timeout": 25_000,
|
||||
}
|
||||
if exe:
|
||||
kwargs["executable_path"] = exe
|
||||
@@ -444,8 +552,7 @@ class _BrowserSession:
|
||||
Path(jarvis).mkdir(parents=True, exist_ok=True)
|
||||
self._context = await engine_obj.launch_persistent_context(jarvis, **kwargs)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
self._page = await self._context.new_page()
|
||||
self._page = await self._adopt_page()
|
||||
print(f"[Browser] ✅ Firefox launched")
|
||||
return
|
||||
|
||||
@@ -457,10 +564,10 @@ class _BrowserSession:
|
||||
"slow_mo": 0,
|
||||
"viewport": None,
|
||||
"no_viewport": True,
|
||||
"timeout": 25_000,
|
||||
}
|
||||
self._context = await engine_obj.launch_persistent_context(safari_profile, **kwargs)
|
||||
await asyncio.sleep(0.5)
|
||||
self._page = await self._context.new_page()
|
||||
self._page = await self._adopt_page()
|
||||
print(f"[Browser] ✅ Safari launched")
|
||||
return
|
||||
|
||||
@@ -471,6 +578,7 @@ class _BrowserSession:
|
||||
"slow_mo": 0,
|
||||
"viewport": None,
|
||||
"no_viewport": True,
|
||||
"timeout": 25_000,
|
||||
"args": [
|
||||
"--start-maximized",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
@@ -493,22 +601,25 @@ class _BrowserSession:
|
||||
|
||||
try:
|
||||
self._context = await engine_obj.launch_persistent_context(profile, **kwargs)
|
||||
await asyncio.sleep(0.5)
|
||||
self._page = await self._context.new_page()
|
||||
self._page = await self._adopt_page()
|
||||
print(f"[Browser] ✅ Launched [{label}] profile={profile}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"[Browser] ⚠️ Real profile failed for {label}: {e}")
|
||||
|
||||
# Gerçek profil açılamadı (tarayıcı zaten açık / kilitli profil / yeni
|
||||
# Chrome sürümleri otomasyonla gerçek profili engelliyor). Kalıcı
|
||||
# JARVIS otomasyon profiline geçilir — buraya bir kez giriş yapılan
|
||||
# hesaplar sonraki oturumlarda da açık kalır.
|
||||
jarvis_profile = str(Path.home() / ".jarvis_profiles" / self.browser_name)
|
||||
Path(jarvis_profile).mkdir(parents=True, exist_ok=True)
|
||||
print(f"[Browser] Retrying with JARVIS profile: {jarvis_profile}")
|
||||
|
||||
try:
|
||||
self._context = await engine_obj.launch_persistent_context(jarvis_profile, **kwargs)
|
||||
await asyncio.sleep(0.5)
|
||||
self._page = await self._context.new_page()
|
||||
print(f"[Browser] ✅ Launched [{label}] with JARVIS profile")
|
||||
self._page = await self._adopt_page()
|
||||
print(f"[Browser] ✅ Launched [{label}] with JARVIS profile "
|
||||
f"(sign-ins persist across sessions)")
|
||||
except Exception as e2:
|
||||
raise RuntimeError(f"Could not launch {self.browser_name}: {e2}") from e2
|
||||
|
||||
@@ -554,13 +665,7 @@ class _BrowserSession:
|
||||
return f"Could not open: {url}"
|
||||
|
||||
async def search(self, query: str, engine: str = "google") -> str:
|
||||
_engines = {
|
||||
"google": "https://www.google.com/search?q=",
|
||||
"bing": "https://www.bing.com/search?q=",
|
||||
"duckduckgo": "https://duckduckgo.com/?q=",
|
||||
"yandex": "https://yandex.com/search/?text=",
|
||||
}
|
||||
base = _engines.get(engine.lower(), _engines["google"])
|
||||
base = _SEARCH_ENGINES.get(engine.lower(), _SEARCH_ENGINES["google"])
|
||||
return await self.go_to(base + query.replace(" ", "+"))
|
||||
|
||||
async def click(self, selector: str = None, text: str = None) -> str:
|
||||
@@ -738,9 +843,26 @@ class _SessionRegistry:
|
||||
"""Tüm aktif tarayıcı oturumlarını yönetir."""
|
||||
|
||||
def __init__(self):
|
||||
self._sessions: dict[str, _BrowserSession] = {}
|
||||
self._active_browser: str = ""
|
||||
self._lock = threading.Lock()
|
||||
self._sessions: dict[str, _BrowserSession] = {}
|
||||
self._active_browser: str = ""
|
||||
self._lock = threading.Lock()
|
||||
self._last_native_url: str = ""
|
||||
|
||||
def has(self, browser_name: str | None = None) -> bool:
|
||||
"""Bu tarayıcı için (veya hiç) aktif bir otomasyon oturumu var mı?"""
|
||||
with self._lock:
|
||||
if not browser_name:
|
||||
return bool(self._sessions)
|
||||
name = _ALIASES.get(browser_name.lower().strip(), browser_name.lower().strip())
|
||||
return name in self._sessions
|
||||
|
||||
def note_native_url(self, url: str) -> None:
|
||||
self._last_native_url = url
|
||||
|
||||
def pop_native_url(self) -> str:
|
||||
"""Son native açılan URL'yi bir kez döndürür (tekrarı önlemek için tüketilir)."""
|
||||
url, self._last_native_url = self._last_native_url, ""
|
||||
return url
|
||||
|
||||
def _get_or_create(self, browser_name: str) -> _BrowserSession:
|
||||
with self._lock:
|
||||
@@ -828,6 +950,53 @@ def browser_control(
|
||||
_log(player, result)
|
||||
return result
|
||||
|
||||
if action == "close":
|
||||
target = browser or _registry._active_browser
|
||||
result = _registry.close_one(target) if target else "No browser specified."
|
||||
_log(player, result)
|
||||
return result
|
||||
|
||||
# ── Gezinme HER ZAMAN native ─────────────────────────────────────────────
|
||||
# go_to / search / new_tab siteyi kullanıcının kendi tarayıcısında açar —
|
||||
# kendi profili, giriş yapılmış hesapları ve açılış sayfasıyla; tıpkı
|
||||
# kullanıcının kendisi açmış gibi. about:blank'li kontrollü pencere burada
|
||||
# asla açılmaz. Tek istisna: hâlihazırda süren bir otomasyon akışı varsa
|
||||
# gezinme o pencerede devam eder (çok adımlı görevler bölünmesin diye).
|
||||
if action in ("go_to", "search", "new_tab"):
|
||||
if _registry.has(browser):
|
||||
sess = _registry.get(browser)
|
||||
try:
|
||||
if action == "search":
|
||||
result = sess.run(sess.search(params.get("query", ""),
|
||||
params.get("engine", "google")))
|
||||
elif action == "new_tab":
|
||||
result = sess.run(sess.new_tab(params.get("url", "")))
|
||||
else:
|
||||
result = sess.run(sess.go_to(params.get("url", "")))
|
||||
except concurrent.futures.TimeoutError:
|
||||
result = f"Browser action '{action}' timed out (60s)."
|
||||
except Exception as e:
|
||||
result = f"Browser error ({action}): {e}"
|
||||
_log(player, result)
|
||||
return result
|
||||
|
||||
if action == "search":
|
||||
base = _SEARCH_ENGINES.get(params.get("engine", "google").lower(),
|
||||
_SEARCH_ENGINES["google"])
|
||||
nav_url = base + params.get("query", "").replace(" ", "+")
|
||||
else:
|
||||
nav_url = params.get("url", "").strip()
|
||||
|
||||
result = _open_native(nav_url, browser)
|
||||
if result.startswith("Opened") and nav_url:
|
||||
_registry.note_native_url(_normalize_url(nav_url))
|
||||
_log(player, result)
|
||||
return result
|
||||
|
||||
# ── Etkileşimli aksiyonlar (tıklama/yazma/okuma…) ────────────────────────
|
||||
# Bunlar fiziksel olarak kontrol edilebilir bir tarayıcı gerektirir;
|
||||
# yalnızca burada otomasyon penceresi açılır ve açılır açılmaz kullanıcının
|
||||
# son gezindiği sayfaya gider — boş sayfada beklemez.
|
||||
try:
|
||||
sess = _registry.get(browser)
|
||||
except Exception as e:
|
||||
@@ -836,11 +1005,14 @@ def browser_control(
|
||||
return result
|
||||
|
||||
try:
|
||||
if action == "go_to":
|
||||
result = sess.run(sess.go_to(params.get("url", "")))
|
||||
elif action == "search":
|
||||
result = sess.run(sess.search(params.get("query", ""), params.get("engine", "google")))
|
||||
elif action == "click":
|
||||
last = _registry.pop_native_url()
|
||||
if last:
|
||||
try:
|
||||
sess.run(sess.go_to(last))
|
||||
except Exception as e:
|
||||
print(f"[Browser] Could not resume last page ({last}): {e}")
|
||||
|
||||
if action == "click":
|
||||
result = sess.run(sess.click(params.get("selector"), params.get("text")))
|
||||
elif action == "type":
|
||||
result = sess.run(sess.type_text(
|
||||
@@ -859,8 +1031,6 @@ def browser_control(
|
||||
result = sess.run(sess.get_url())
|
||||
elif action == "press":
|
||||
result = sess.run(sess.press(params.get("key", "Enter")))
|
||||
elif action == "new_tab":
|
||||
result = sess.run(sess.new_tab(params.get("url", "")))
|
||||
elif action == "close_tab":
|
||||
result = sess.run(sess.close_tab())
|
||||
elif action == "screenshot":
|
||||
@@ -871,9 +1041,6 @@ def browser_control(
|
||||
result = sess.run(sess.forward())
|
||||
elif action == "reload":
|
||||
result = sess.run(sess.reload())
|
||||
elif action == "close":
|
||||
target = browser or _registry._active_browser
|
||||
result = _registry.close_one(target) if target else "No browser specified."
|
||||
else:
|
||||
result = f"Unknown browser action: '{action}'"
|
||||
|
||||
|
||||
+43
-33
@@ -110,43 +110,53 @@ def _image_to_base64(path: Path) -> str:
|
||||
return base64.b64encode(path.read_bytes()).decode("utf-8")
|
||||
|
||||
|
||||
_VALID_INTENTS = {"write", "edit", "explain", "run", "build", "screen_debug", "optimize"}
|
||||
|
||||
|
||||
def _detect_intent(description: str, file_path: str, code: str) -> str:
|
||||
desc = (description or "").lower()
|
||||
"""
|
||||
Dil bağımsız niyet tespiti — sabit anahtar kelime listesi YOK.
|
||||
Kullanıcı hangi dilde konuşursa konuşsun, açıklama Gemini'ye
|
||||
sınıflandırtılır. API'ye ulaşılamazsa dile bakmayan yapısal
|
||||
ipuçlarına (dosya diskte var mı, kod verilmiş mi) düşülür.
|
||||
"""
|
||||
desc = (description or "").strip()
|
||||
file_exists = bool(file_path) and Path(file_path).exists()
|
||||
|
||||
screen_kw = ["ekrandaki", "screen", "ekranda", "bu hatayı", "why am i getting",
|
||||
"neden hata", "what's wrong", "ne yanlış", "screenshot", "görüntü"]
|
||||
if any(k in desc for k in screen_kw):
|
||||
return "screen_debug"
|
||||
if desc:
|
||||
try:
|
||||
ctx = []
|
||||
if file_path:
|
||||
ctx.append(f"a file path is provided (exists on disk: {file_exists})")
|
||||
if code:
|
||||
ctx.append("an inline code snippet is provided")
|
||||
prompt = (
|
||||
"Classify a coding assistant request into exactly ONE intent word.\n"
|
||||
"The request may be written in ANY language.\n\n"
|
||||
f"Request: {desc}\n"
|
||||
+ (f"Context: {'; '.join(ctx)}\n" if ctx else "")
|
||||
+ "\nIntents:\n"
|
||||
" write = create new code from scratch\n"
|
||||
" edit = modify an existing file\n"
|
||||
" explain = describe what given code/file does\n"
|
||||
" run = execute an existing file\n"
|
||||
" build = write code, run it, and iterate until it works\n"
|
||||
" screen_debug = analyze an error currently visible on the user's screen\n"
|
||||
" optimize = refactor / clean up / speed up existing code\n\n"
|
||||
"Reply with ONLY the intent word, nothing else."
|
||||
)
|
||||
ans = _get_gemini().generate_content(prompt).text.strip().lower()
|
||||
ans = ans.strip("`'\". \n")
|
||||
if ans in _VALID_INTENTS:
|
||||
return ans
|
||||
except Exception as e:
|
||||
print(f"[Code] Intent classification failed ({e}) — structural fallback")
|
||||
|
||||
optimize_kw = ["optimize", "refactor", "clean up", "improve", "temizle",
|
||||
"iyileştir", "daha iyi", "make it better", "hızlandır"]
|
||||
if any(k in desc for k in optimize_kw) and (code or file_path):
|
||||
return "optimize"
|
||||
|
||||
if file_path:
|
||||
p = Path(file_path)
|
||||
edit_kw = ["edit", "update", "modify", "change", "add", "remove",
|
||||
"refactor", "fix", "rename", "replace", "düzenle", "değiştir"]
|
||||
run_kw = ["run", "execute", "launch", "start", "çalıştır"]
|
||||
build_kw = ["build", "make it work", "try", "attempt"]
|
||||
|
||||
if p.exists() and any(k in desc for k in edit_kw):
|
||||
return "edit"
|
||||
if p.exists() and any(k in desc for k in run_kw):
|
||||
return "run"
|
||||
if any(k in desc for k in build_kw):
|
||||
return "build"
|
||||
if p.exists():
|
||||
return "explain"
|
||||
|
||||
explain_kw = ["explain", "what does", "describe", "analyze", "açıkla", "ne yapıyor"]
|
||||
if any(k in desc for k in explain_kw) and (code or file_path):
|
||||
# Yapısal geri dönüş — hiçbir dile bağlı değil
|
||||
if file_exists:
|
||||
return "edit" if desc else "explain"
|
||||
if code:
|
||||
return "explain"
|
||||
|
||||
build_kw = ["build", "make it work", "try and", "attempt"]
|
||||
if any(k in desc for k in build_kw):
|
||||
return "build"
|
||||
|
||||
return "write"
|
||||
|
||||
def _write(description: str, language: str, output_path: str, player=None) -> tuple[str, Path]:
|
||||
|
||||
@@ -206,7 +206,14 @@ def maximize_window():
|
||||
def snap_left():
|
||||
if _OS == "Windows":
|
||||
pyautogui.hotkey("win", "left")
|
||||
elif _OS == "Linux":
|
||||
elif _OS == "Darwin":
|
||||
# macOS has no built-in snap; try Rectangle app shortcut if installed
|
||||
try:
|
||||
subprocess.run(["open", "-a", "Rectangle"], capture_output=True, timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
pyautogui.hotkey("ctrl", "option", "left")
|
||||
else: # Linux
|
||||
try:
|
||||
subprocess.run(["wmctrl", "-r", ":ACTIVE:", "-e", "0,0,0,960,1080"],
|
||||
capture_output=True)
|
||||
@@ -216,7 +223,13 @@ def snap_left():
|
||||
def snap_right():
|
||||
if _OS == "Windows":
|
||||
pyautogui.hotkey("win", "right")
|
||||
elif _OS == "Linux":
|
||||
elif _OS == "Darwin":
|
||||
try:
|
||||
subprocess.run(["open", "-a", "Rectangle"], capture_output=True, timeout=1)
|
||||
except Exception:
|
||||
pass
|
||||
pyautogui.hotkey("ctrl", "option", "right")
|
||||
else: # Linux
|
||||
try:
|
||||
subprocess.run(["wmctrl", "-r", ":ACTIVE:", "-e", "0,960,0,960,1080"],
|
||||
capture_output=True)
|
||||
|
||||
+17
-1
@@ -34,7 +34,7 @@ _APP_ALIASES: dict[str, dict[str, str]] = {
|
||||
"vscode": {"Windows": "code", "Darwin": "Visual Studio Code", "Linux": "code"},
|
||||
"visual studio code": {"Windows": "code", "Darwin": "Visual Studio Code", "Linux": "code"},
|
||||
"code": {"Windows": "code", "Darwin": "Visual Studio Code", "Linux": "code"},
|
||||
"terminal": {"Windows": "wt", "Darwin": "Terminal", "Linux": "gnome-terminal"},
|
||||
"terminal": {"Windows": "wt", "Darwin": "Terminal", "Linux": "x-terminal-emulator"},
|
||||
"cmd": {"Windows": "cmd.exe", "Darwin": "Terminal", "Linux": "bash"},
|
||||
"powershell": {"Windows": "powershell.exe", "Darwin": "Terminal", "Linux": "bash"},
|
||||
"postman": {"Windows": "Postman", "Darwin": "Postman", "Linux": "postman"},
|
||||
@@ -168,8 +168,24 @@ def _launch_macos(app_name: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
_LINUX_TERMINAL_FALLBACKS = [
|
||||
"x-terminal-emulator", "gnome-terminal", "konsole", "xfce4-terminal",
|
||||
"xterm", "lxterminal", "mate-terminal", "tilix", "alacritty", "kitty",
|
||||
]
|
||||
|
||||
def _launch_linux(app_name: str) -> bool:
|
||||
|
||||
# terminal emulators: try common ones in order
|
||||
if app_name in ("x-terminal-emulator", "gnome-terminal", "terminal"):
|
||||
for term in _LINUX_TERMINAL_FALLBACKS:
|
||||
if shutil.which(term):
|
||||
try:
|
||||
subprocess.Popen([term], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
time.sleep(1.0)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
binary = (
|
||||
shutil.which(app_name) or
|
||||
shutil.which(app_name.lower()) or
|
||||
|
||||
+6
-7
@@ -19,13 +19,12 @@ def _base_dir() -> Path:
|
||||
|
||||
|
||||
def _get_os() -> str:
|
||||
try:
|
||||
cfg = json.loads(
|
||||
(_base_dir() / "config" / "api_keys.json").read_text(encoding="utf-8")
|
||||
)
|
||||
return cfg.get("os_system", "windows").lower()
|
||||
except Exception:
|
||||
return "windows"
|
||||
_sys = platform.system()
|
||||
if _sys == "Darwin":
|
||||
return "mac"
|
||||
if _sys == "Linux":
|
||||
return "linux"
|
||||
return "windows"
|
||||
|
||||
|
||||
def _scripts_dir() -> Path:
|
||||
|
||||
@@ -52,6 +52,8 @@ from actions.computer_control import computer_control
|
||||
from actions.game_updater import game_updater
|
||||
from actions.system_monitor import SystemMonitor, get_system_status
|
||||
from actions.proactive import ProactiveEngine
|
||||
from actions.web_search import _news as _fetch_news_sync
|
||||
from memory.config_manager import get_brief_enabled
|
||||
|
||||
|
||||
def get_base_dir():
|
||||
@@ -248,6 +250,9 @@ TOOL_DECLARATIONS = [
|
||||
"description": (
|
||||
"Controls any web browser. Use for: opening websites, searching the web, "
|
||||
"clicking elements, filling forms, scrolling, screenshots, navigation, any web-based task. "
|
||||
"Simple open/search requests launch the user's own browser normally (their real profile "
|
||||
"and logged-in accounts); interactive actions (click, type, fill_form...) attach an "
|
||||
"automation browser. "
|
||||
"Always pass the 'browser' parameter when the user specifies a browser (e.g. 'open in Edge', "
|
||||
"'use Firefox', 'open Chrome'). Multiple browsers can run simultaneously."
|
||||
),
|
||||
@@ -520,6 +525,7 @@ class JarvisLive:
|
||||
|
||||
def __init__(self, ui: JarvisUI):
|
||||
self.ui = ui
|
||||
self._asst_name = "JARVIS" # updated each session from config
|
||||
self.session = None
|
||||
self.audio_in_queue = None
|
||||
self.out_queue = None
|
||||
@@ -613,6 +619,15 @@ class JarvisLive:
|
||||
def _build_config(self) -> types.LiveConnectConfig:
|
||||
from datetime import datetime
|
||||
|
||||
# Load customization from config
|
||||
try:
|
||||
_cfg = json.loads(open(API_CONFIG_PATH, encoding="utf-8").read())
|
||||
self._asst_name = (_cfg.get("assistant_name") or "JARVIS").strip()
|
||||
_user_name = (_cfg.get("user_name") or "").strip()
|
||||
except Exception:
|
||||
self._asst_name = "JARVIS"
|
||||
_user_name = ""
|
||||
|
||||
memory = load_memory()
|
||||
mem_str = format_memory_for_prompt(memory)
|
||||
sys_prompt = _load_system_prompt()
|
||||
@@ -625,7 +640,19 @@ class JarvisLive:
|
||||
f"Use this to calculate exact times for reminders.\n\n"
|
||||
)
|
||||
|
||||
parts = [time_ctx]
|
||||
# Identity injection — overrides any hardcoded name in prompt.txt
|
||||
_addr = (f"ADDRESS: Always call the user '{_user_name}'."
|
||||
if _user_name
|
||||
else "ADDRESS: When speaking Turkish → always say \"efendim\". "
|
||||
"When speaking English → say \"sir\". Never mix languages.")
|
||||
identity_ctx = (
|
||||
f"[IDENTITY]\n"
|
||||
f"Your name is {self._asst_name}. "
|
||||
f"Always refer to yourself as {self._asst_name}.\n"
|
||||
f"{_addr}\n\n"
|
||||
)
|
||||
|
||||
parts = [time_ctx, identity_ctx]
|
||||
if mem_str:
|
||||
parts.append(mem_str)
|
||||
parts.append(sys_prompt)
|
||||
@@ -725,9 +752,8 @@ class JarvisLive:
|
||||
self._pending_vision = (img_b, mime_t, user_text, angle)
|
||||
result = (
|
||||
f"[VISION_ACTIVE] {_stall.capitalize()} captured. "
|
||||
f"Immediately say ONE natural sentence in the user's language "
|
||||
f"(e.g. 'Looking at your {_stall} now, sir' / "
|
||||
f"'{'Kameraya' if _stall == 'camera' else 'Ekrana'} bakıyorum efendim'). "
|
||||
f"Immediately say ONE short natural sentence in the user's own language, "
|
||||
f"telling them you are looking at their {_stall} right now. "
|
||||
f"Do NOT describe or guess content — the actual image arrives in the NEXT message."
|
||||
)
|
||||
|
||||
@@ -905,7 +931,7 @@ class JarvisLive:
|
||||
|
||||
full_out = " ".join(out_buf).strip()
|
||||
if full_out:
|
||||
self.ui.write_log(f"Jarvis: {full_out}")
|
||||
self.ui.write_log(f"{self._asst_name}: {full_out}")
|
||||
if self._dashboard:
|
||||
asyncio.create_task(self._dashboard.broadcast({
|
||||
"type": "log", "speaker": "jarvis",
|
||||
@@ -1003,15 +1029,13 @@ class JarvisLive:
|
||||
|
||||
async def _send_startup_briefing(self) -> None:
|
||||
"""
|
||||
Two-phase briefing for instant perceived response:
|
||||
Phase 1 — immediate greeting (no tools, no fetch) → Jarvis speaks in <2s
|
||||
Phase 2 — news fetched in background, injected after greeting finishes
|
||||
Two-phase briefing optimized for speed:
|
||||
Phase 1 — instant greeting (no tools) → speech starts in <1s
|
||||
Phase 2 — news pre-fetched in a background thread while Phase 1 plays,
|
||||
delivered as ready text (no Gemini tool-call round-trip) and
|
||||
shown on the UI content panel. Waits for turn_complete event
|
||||
instead of a fixed sleep so there is no unnecessary gap.
|
||||
"""
|
||||
await asyncio.sleep(0.3)
|
||||
if not self.session:
|
||||
return
|
||||
|
||||
# ── memory ───────────────────────────────────────────────────────────
|
||||
memory = load_memory()
|
||||
identity = memory.get("identity", {})
|
||||
|
||||
@@ -1021,59 +1045,87 @@ class JarvisLive:
|
||||
|
||||
lang = _val("language")
|
||||
name = _val("name")
|
||||
|
||||
from datetime import datetime
|
||||
time_str = datetime.now().strftime("%H:%M")
|
||||
|
||||
# ── Phase 1: instant greeting — one simple sentence ──────────────────
|
||||
# Start fetching news immediately — runs in parallel while phase 1 plays
|
||||
loop = asyncio.get_event_loop()
|
||||
news_future = loop.run_in_executor(None, _fetch_news_sync, "top world news today")
|
||||
|
||||
await asyncio.sleep(0.3)
|
||||
if not self.session:
|
||||
return
|
||||
|
||||
# ── Phase 1: instant greeting ─────────────────────────────────────────
|
||||
lang_clause = f" Respond in {lang}." if lang else ""
|
||||
name_clause = f" Address the user as {name}." if name else ""
|
||||
p1 = (
|
||||
f"Greet the user, mention it is {time_str}, and say you are fetching today's news headlines now. "
|
||||
f"Greet the user, mention it is {time_str}, and say you are fetching today's news now. "
|
||||
f"One short sentence only. Do not call any tools.{lang_clause}{name_clause}"
|
||||
)
|
||||
|
||||
# Clear the turn-done event so we can wait for Phase 1 to finish
|
||||
if self._turn_done_event:
|
||||
self._turn_done_event.clear()
|
||||
|
||||
await self.session.send_client_content(
|
||||
turns={"parts": [{"text": p1}]},
|
||||
turn_complete=True,
|
||||
)
|
||||
self.ui.write_log("SYS: Briefing phase 1 (greeting) sent.")
|
||||
|
||||
# ── Phase 2: fetch news in background, deliver after greeting plays ───
|
||||
async def _guarded_news():
|
||||
# ── Phase 2: fire as soon as Phase 1 audio is done ───────────────────
|
||||
async def _deliver_news():
|
||||
try:
|
||||
await self._briefing_news_phase(lang)
|
||||
lang_str = f" Respond in {lang}." if lang else ""
|
||||
|
||||
# Wait for news fetch (already running) and Phase 1 turn-complete
|
||||
# in parallel — whichever takes longer determines the wait time
|
||||
news_done = asyncio.wrap_future(news_future)
|
||||
turn_waited = False
|
||||
if self._turn_done_event:
|
||||
try:
|
||||
await asyncio.wait_for(self._turn_done_event.wait(), timeout=6.0)
|
||||
turn_waited = True
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
# If turn_complete didn't fire (timeout), give a small buffer
|
||||
if not turn_waited:
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
try:
|
||||
news_text = await asyncio.wait_for(news_done, timeout=4.0)
|
||||
except Exception:
|
||||
news_text = ""
|
||||
|
||||
if not self.session:
|
||||
return
|
||||
|
||||
if news_text and len(news_text) > 60:
|
||||
# Show on UI content panel immediately
|
||||
self.ui.show_content("NEWS — top world news today", news_text)
|
||||
|
||||
p2 = (
|
||||
f"[BRIEFING] Here are today's top news headlines:\n{news_text}\n\n"
|
||||
"Pick ONE headline, summarise it in one sentence, then say the full list "
|
||||
f"is displayed on screen. Do not call any tools.{lang_str}"
|
||||
)
|
||||
else:
|
||||
p2 = (
|
||||
"News headlines could not be fetched right now. "
|
||||
f"Let the user know briefly.{lang_str}"
|
||||
)
|
||||
|
||||
await self.session.send_client_content(
|
||||
turns={"parts": [{"text": p2}]},
|
||||
turn_complete=True,
|
||||
)
|
||||
self.ui.write_log("SYS: Briefing phase 2 (news) sent.")
|
||||
except Exception as e:
|
||||
print(f"[Briefing] Phase 2 error: {e}")
|
||||
self.ui.write_log(f"SYS: Briefing news phase failed: {e}")
|
||||
asyncio.create_task(_guarded_news())
|
||||
self.ui.write_log(f"SYS: Briefing phase 2 failed: {e}")
|
||||
|
||||
async def _briefing_news_phase(self, lang: str) -> None:
|
||||
"""
|
||||
Sends phase-2 (news) to Gemini ~1.5 s after phase-1 is dispatched so
|
||||
Gemini starts working on it while phase-1 audio is still playing.
|
||||
"""
|
||||
lang_str = f" Respond in {lang}." if lang else ""
|
||||
|
||||
# 1.5 s is enough for Gemini to finish generating phase-1 audio on its
|
||||
# side (turn_complete) while the greeting is still being played locally.
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
if not self.session:
|
||||
return
|
||||
|
||||
p2 = (
|
||||
"[BRIEFING] Call web_search with mode='news' and query='top world news today' "
|
||||
"to find actual recent news articles with real event headlines (not just website names). "
|
||||
"After the search, say ONE specific news event from the results in one sentence, "
|
||||
f"then say the full list is displayed on screen.{lang_str}"
|
||||
)
|
||||
|
||||
await self.session.send_client_content(
|
||||
turns={"parts": [{"text": p2}]},
|
||||
turn_complete=True,
|
||||
)
|
||||
self.ui.write_log("SYS: Briefing phase 2 (news) sent.")
|
||||
asyncio.create_task(_deliver_news())
|
||||
|
||||
# ── System monitor ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1242,8 +1294,8 @@ class JarvisLive:
|
||||
if self._dashboard:
|
||||
tg.create_task(self._relay_phone_audio())
|
||||
|
||||
# Morning briefing — fires once per process launch
|
||||
if not self._briefing_sent:
|
||||
# Morning briefing — fires once per process launch (if enabled)
|
||||
if not self._briefing_sent and get_brief_enabled():
|
||||
self._briefing_sent = True
|
||||
tg.create_task(self._send_startup_briefing())
|
||||
|
||||
|
||||
@@ -48,4 +48,44 @@ def get_gemini_key() -> str | None:
|
||||
|
||||
def is_configured() -> bool:
|
||||
key = get_gemini_key()
|
||||
return bool(key and len(key) > 15)
|
||||
return bool(key and len(key) > 15)
|
||||
|
||||
|
||||
def get_assistant_name() -> str:
|
||||
"""Return the configured assistant name, or 'JARVIS' if not set."""
|
||||
return load_api_keys().get("assistant_name", "JARVIS") or "JARVIS"
|
||||
|
||||
|
||||
def get_user_name() -> str:
|
||||
"""Return the configured user name for addressing."""
|
||||
return load_api_keys().get("user_name", "")
|
||||
|
||||
|
||||
def save_assistant_config(assistant_name: str, user_name: str) -> None:
|
||||
"""Persist assistant name and user name to config."""
|
||||
ensure_config_dir()
|
||||
data: dict = {}
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
data = {}
|
||||
data["assistant_name"] = assistant_name.strip() or "JARVIS"
|
||||
data["user_name"] = user_name.strip()
|
||||
CONFIG_FILE.write_text(json.dumps(data, indent=4), encoding="utf-8")
|
||||
|
||||
|
||||
def get_brief_enabled() -> bool:
|
||||
return load_api_keys().get("morning_brief_enabled", True)
|
||||
|
||||
|
||||
def save_brief_enabled(enabled: bool) -> None:
|
||||
ensure_config_dir()
|
||||
data: dict = {}
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
data = {}
|
||||
data["morning_brief_enabled"] = enabled
|
||||
CONFIG_FILE.write_text(json.dumps(data, indent=4), encoding="utf-8")
|
||||
@@ -1,4 +1,4 @@
|
||||
# ⚙️ MARK XLVIII (48)
|
||||
# ⚙️ MARK XLIX
|
||||
### The Ultimate Cross-Platform Personal AI Assistant — By FatihMakes
|
||||
|
||||
> 📺 **[Watch the full setup video on YouTube](https://www.youtube.com/@FatihMakes)**
|
||||
@@ -9,9 +9,7 @@ A real-time voice AI that can hear, see, understand, and control your computer
|
||||
|
||||
## ✨ Overview
|
||||
|
||||
MARK XLVIII is a direct evolution of Mark XLVII — sharper, faster, and more natural to talk to. This build focused entirely on removing every friction point: the silence while JARVIS "thinks," the awkward double-responses, the interrupts that took four seconds to land, the news results that linked to homepages instead of articles. Everything that made the experience feel like software instead of an assistant has been addressed.
|
||||
|
||||
It's not just an assistant — it's an extension of your digital life.
|
||||
MARK XLIX deepens the personal assistant foundation. Rather than adding more tools, this build focused on making the assistant truly *yours*: it starts with your computer, learns your name, and pays attention to what you're doing. The goal before the plugin era begins is a core that feels alive — not just reactive.
|
||||
|
||||
---
|
||||
|
||||
@@ -27,63 +25,61 @@ It's not just an assistant — it's an extension of your digital life.
|
||||
| 🧠 Persistent Memory | Deeply remembers projects, preferences, and personal context across sessions |
|
||||
| ⌨️ Hybrid Input | Seamlessly switch between keyboard typing and voice commands |
|
||||
| 🌅 Morning Briefing | On first boot: greets you, reads the time, fetches live news headlines, and checks weather |
|
||||
| 🔔 Proactive Check-ins | After 15 minutes of silence JARVIS checks context and offers something genuinely useful — no hardcoded rules, Gemini decides |
|
||||
| 📊 Hardware Monitoring | Continuous CPU, RAM, GPU and temperature telemetry with localized voice alerts when thresholds are breached |
|
||||
| 🔔 Proactive Check-ins | After 15 minutes of silence, checks context and offers something genuinely useful |
|
||||
| 📊 Hardware Monitoring | Continuous CPU, RAM, GPU and temperature telemetry with localized voice alerts |
|
||||
| 🌤️ Weather Report | Live weather data for your city, personalized from memory |
|
||||
| 🗺️ Dynamic Content Panel | Scrollable display layer beneath the HUD that renders web results, news, and search data with timestamps |
|
||||
| 🗺️ Dynamic Content Panel | Scrollable display layer beneath the HUD that renders web results, news, and search data |
|
||||
| 🔍 Multi-Mode Web Search | `news` / `research` / `price` / `compare` / `search` — Gemini Grounded first, DDG fallback |
|
||||
| ⏰ Smart Reminders | OS-native scheduled notifications (Windows Task Scheduler / macOS LaunchAgent / Linux systemd) |
|
||||
| ✈️ Flight Finder | Live flight price and availability lookup |
|
||||
| 🎮 Game Updater | Checks and triggers game updates on demand |
|
||||
| 🎮 Game Updater | Checks and triggers game updates on Steam and Epic Games on demand |
|
||||
| 📂 File Processor | Read, summarize, and answer questions about local files |
|
||||
| 💻 Code Helper | Inline code review, debugging, and generation |
|
||||
| 🌐 Browser Control | Open URLs, navigate tabs, and interact with the browser by voice |
|
||||
| 📨 Send Message | Compose and send messages through integrated messaging apps |
|
||||
| 📨 Send Message | Compose and send messages through WhatsApp, Telegram, and more |
|
||||
| 🎬 YouTube Control | Search, play, and control YouTube playback by voice |
|
||||
| 🖱️ Desktop Control | Taskbar, window management, and desktop-level operations |
|
||||
| 🧑💻 Silent Language Memory | Detects spoken language on first use and saves it silently — all future sessions and briefings adapt automatically |
|
||||
| 🧑💻 Silent Language Memory | Detects spoken language on first use and saves it — all future sessions adapt automatically |
|
||||
| 📱 Remote Dashboard | Control the assistant from your phone via QR code pairing |
|
||||
|
||||
---
|
||||
|
||||
## 🆕 What's New in XLVIII
|
||||
## 🆕 What's New in XLIX
|
||||
|
||||
### ✋ Instant Interrupt — ESC or Button
|
||||
Press **Escape** or click the `INTERRUPT` button to cut JARVIS off mid-sentence and return to listening. Previously, cancelling a response required waiting for the full audio buffer to drain — sometimes four seconds. In Mark XLVIII, audio is split into ~50 ms chunks (2400 bytes each) so the interrupt fires within one slice. The interrupt drains the audio queue, sets a flag, and clears the turn — listening resumes in under 100 ms.
|
||||
### ⚡ Auto-Start on Boot
|
||||
The assistant now registers itself with the operating system's startup system. One click in the UI toggles it on or off. On Windows, it writes to the registry using `pythonw.exe` so no console window ever appears. On macOS it installs a LaunchAgent plist; on Linux a `.desktop` autostart entry. The button reflects the current state every time the app launches.
|
||||
|
||||
### 👁️ Immediate Vision Acknowledgment
|
||||
When you ask JARVIS to look at your screen or camera, it no longer goes silent while processing. The tool now instructs Gemini to immediately say a natural sentence ("Looking at your screen now, sir" / "Ekrana bakıyorum efendim") while the image capture runs. The actual analysis follows as a second response. No more awkward silence.
|
||||
### 🎨 Assistant Customization
|
||||
The assistant is no longer locked to the name "JARVIS". Click `⚙ CUSTOMISE ASSISTANT` in the right panel to change:
|
||||
- **Assistant name** — displayed everywhere in the UI (title bar, header, HUD, log, footer) and injected into the Gemini system prompt so the AI knows its own name
|
||||
- **Your name** — how the assistant addresses you. Leave blank for the default language-aware addressing (`sir` / `efendim`), or set your actual name for a more personal feel
|
||||
|
||||
### 📰 Parallel News Search — First Result Wins
|
||||
News queries now run **Gemini Grounded Search and DuckDuckGo news simultaneously** in two daemon threads. Whichever delivers a valid result first wins; the other is silently discarded. Previously, a Gemini 503 error would stall the search for several seconds before falling back to DDG. Now the fallback happens in parallel — total news fetch time drops to whichever backend is fastest at that moment.
|
||||
Changes take effect immediately without restarting.
|
||||
|
||||
### 🗞️ Real News Articles (Not Homepages)
|
||||
DDG news search was previously using `ddgs.text()`, which returned website homepage URLs for news queries. Mark XLVIII switches to `ddgs.news()`, which returns actual article URLs, titles, snippets, and source names — exactly what you want in a news briefing.
|
||||
### 📋 Clipboard Intelligence
|
||||
Copy any text of 10 or more characters and a floating panel appears at the bottom of the window. Four quick actions — **TRANSLATE**, **SUMMARISE**, **EXPLAIN**, **FIX** — send the copied content directly to the assistant with one click. The panel auto-dismisses after 8 seconds. This turns the clipboard into a silent command channel for anything on your screen.
|
||||
|
||||
### 🌅 Two-Phase Startup Briefing — Runs Concurrently
|
||||
The startup briefing now sends Phase 2 (news fetch) while Phase 1 audio (the greeting) is still playing. Previously, Phase 2 waited for Phase 1 to fully complete. The 1.5-second overlap means the news headline is ready by the time JARVIS finishes saying "Good morning."
|
||||
### ☀ Morning Brief Toggle + Speed Optimization
|
||||
The morning briefing can now be turned on or off with one click from the settings drawer (`⚙` → `☀ MORNING BRIEF: ON/OFF`). Users who don't want a startup briefing can disable it permanently; the setting survives restarts. The briefing itself was also re-engineered: news is now pre-fetched in a background thread the moment the session starts, running in parallel while the greeting plays. By the time the greeting finishes, the results are already ready — no extra Gemini tool-call round-trip needed. Briefing delivery is noticeably faster as a result.
|
||||
|
||||
### 🔁 Smarter Reconnection — Exponential Backoff
|
||||
Network timeouts now use exponential backoff: 3s → 6s → 12s → 60s (capped). Each retry shows a Turkish-language status message in the UI ("Bağlantı kurulamadı — Xs sonra tekrar deneniyor"). Previously, a dropped connection would loop tightly and show no useful information.
|
||||
---
|
||||
|
||||
### 🛡️ Vision Cooldown & Echo Guard
|
||||
Screen capture is guarded against echo loops. If JARVIS speaks about the screen and the microphone picks up its own voice, a secondary `screen_process` call can be triggered. Mark XLVIII blocks duplicate calls with a 4-second cooldown and a `_vision_busy` flag. Both are fully reset when a new session connects — fixing a bug where the cooldown would persist across reconnects and block legitimate requests.
|
||||
## 🗺️ Mark Roadmap
|
||||
|
||||
### 🌐 Language-Aware Address — Never Mixed
|
||||
The system prompt now enforces: **Turkish → efendim**, **English → sir**, never mixed. Mark XLVII's prompt said "Always call sir to user" which caused JARVIS to say "sir" mid-Turkish sentence. Fixed.
|
||||
|
||||
### 🪟 Zero Terminal Windows
|
||||
A subprocess monkey-patch at startup sets `CREATE_NO_WINDOW` on every child process launched by the app. No PowerShell, no CMD, no terminal flash — ever. Applies to all actions including reminders, system commands, and scheduler calls.
|
||||
|
||||
### 🔄 Session State Isolation
|
||||
All transient vision and interrupt flags (`_pending_vision`, `_vision_busy`, `_vision_cam_active`, `_vision_close_pending`, `_interrupted`) are fully reset whenever a new Gemini session connects. Previously, state from a crashed session could carry over and leave JARVIS in a broken state until restart.
|
||||
| Mark | Focus |
|
||||
|---|---|
|
||||
| **XLVIII** | Instant interrupt · parallel news · two-phase briefing · exponential backoff · vision cooldown |
|
||||
| **XLIX** | Auto-start · clipboard intelligence · assistant customization |
|
||||
| **L** | Wake word · proactive system 2.0 · session memory / daily continuity |
|
||||
| **LI+** | Plugin system · email · quiz mode · calorie counter · and more |
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/FatihMakes/Mark-XLVIII.git
|
||||
cd Mark-XLVIII
|
||||
git clone https://github.com/FatihMakes/Mark-XLIX.git
|
||||
cd Mark-XLIX
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
@@ -106,7 +102,7 @@ python main.py
|
||||
## 🗂️ Project Structure
|
||||
|
||||
```
|
||||
Mark XLVIII/
|
||||
Mark XLIX/
|
||||
├── main.py # Core loop — Gemini Live session, audio I/O, tool dispatch
|
||||
├── ui.py # PyQt6 HUD — waveform, log panel, interrupt button, camera feed
|
||||
├── setup.py # First-run configuration wizard
|
||||
@@ -125,16 +121,16 @@ Mark XLVIII/
|
||||
│ ├── weather_report.py # Live weather data
|
||||
│ ├── flight_finder.py # Flight search
|
||||
│ ├── youtube_video.py # YouTube playback control
|
||||
│ ├── game_updater.py # Game update management
|
||||
│ ├── game_updater.py # Game update management (Steam / Epic)
|
||||
│ ├── code_helper.py # Code review and generation
|
||||
│ ├── dev_agent.py # Developer task agent
|
||||
│ ├── desktop.py # Desktop and taskbar control
|
||||
│ └── proactive.py # Proactive silence-break suggestions
|
||||
│ └── proactive.py # Proactive silence-break suggestions
|
||||
├── memory/ # Persistent key-value memory store
|
||||
├── core/
|
||||
│ └── prompt.txt # JARVIS personality and tool-routing rules
|
||||
│ └── prompt.txt # Assistant personality and tool-routing rules
|
||||
└── config/
|
||||
└── api_keys.json # API key and system configuration
|
||||
└── api_keys.json # API key, OS setting, assistant name, user name
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
PyQt6
|
||||
sounddevice
|
||||
google-genai
|
||||
google-generativeai
|
||||
@@ -24,4 +25,5 @@ comtypes; sys_platform == "win32"
|
||||
pycaw; sys_platform == "win32"
|
||||
win10toast; sys_platform == "win32"
|
||||
pywinauto; sys_platform == "win32"
|
||||
pywin32; sys_platform == "win32"
|
||||
qrcode[pil]
|
||||
@@ -1,5 +1,7 @@
|
||||
import subprocess
|
||||
import sys
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
print("Installing requirements...")
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], check=True)
|
||||
@@ -7,5 +9,18 @@ subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"
|
||||
print("Installing Playwright browsers...")
|
||||
subprocess.run([sys.executable, "-m", "playwright", "install"], check=True)
|
||||
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
import win32com.client # noqa: F401
|
||||
except ImportError:
|
||||
postinstall = Path(sys.executable).parent / "Scripts" / "pywin32_postinstall.py"
|
||||
print(
|
||||
"\n⚠️ pywin32 did not install correctly — desktop shortcut creation "
|
||||
"will fall back to a slower method that may not work on this machine.\n"
|
||||
" Try fixing it manually with:\n"
|
||||
f' "{sys.executable}" -m pip install --force-reinstall pywin32\n'
|
||||
f' "{sys.executable}" "{postinstall}" -install\n'
|
||||
)
|
||||
|
||||
print("\n✅ Setup complete! Run 'python main.py' to start MARK XXV.")
|
||||
|
||||
Reference in new issue
Block a user