diff --git a/backend/apps/analytics/analytics.py b/backend/apps/analytics/analytics.py index f2bd6a2c..ecc67ab4 100644 --- a/backend/apps/analytics/analytics.py +++ b/backend/apps/analytics/analytics.py @@ -15,7 +15,7 @@ from backend.apps.analytics.collector import init as init_collector, shutdown as logger = logging.getLogger(__name__) -APP_VERSION = "1.0.22" +APP_VERSION = "1.0.23" _heartbeat_task: asyncio.Task | None = None diff --git a/backend/mcp-bundles/reddit-mcp-buddy.js b/backend/mcp-bundles/reddit-mcp-buddy.js index b57f77a6..d50ba6d4 100644 --- a/backend/mcp-bundles/reddit-mcp-buddy.js +++ b/backend/mcp-bundles/reddit-mcp-buddy.js @@ -19352,6 +19352,17 @@ var requestPrototype = { } }); }); +Object.defineProperty(requestPrototype, /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom"), { + value: function(depth, options, inspectFn) { + const props = { + method: this.method, + url: this.url, + headers: this.headers, + nativeRequest: this[requestCache] + }; + return `Request (lightweight) ${inspectFn(props, { ...options, depth: depth == null ? null : depth - 1 })}`; + } +}); Object.setPrototypeOf(requestPrototype, Request.prototype); var newRequest = (incoming, defaultHostname) => { const req = Object.create(requestPrototype); @@ -19456,6 +19467,17 @@ var Response2 = class _Response { } }); }); +Object.defineProperty(Response2.prototype, /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom"), { + value: function(depth, options, inspectFn) { + const props = { + status: this.status, + headers: this.headers, + ok: this.ok, + nativeResponse: this[responseCache] + }; + return `Response (lightweight) ${inspectFn(props, { ...options, depth: depth == null ? null : depth - 1 })}`; + } +}); Object.setPrototypeOf(Response2, GlobalResponse); Object.setPrototypeOf(Response2.prototype, GlobalResponse.prototype); async function readWithoutBlocking(readPromise) { diff --git a/backend/npm-servers/notionhq-notion-mcp-server/package-lock.json b/backend/npm-servers/notionhq-notion-mcp-server/package-lock.json index d9c5681e..d1bc7c56 100644 --- a/backend/npm-servers/notionhq-notion-mcp-server/package-lock.json +++ b/backend/npm-servers/notionhq-notion-mcp-server/package-lock.json @@ -13,9 +13,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.13", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", - "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -1010,9 +1010,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -1182,9 +1182,9 @@ } }, "node_modules/hono": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", - "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", "engines": { "node": ">=16.9.0" diff --git a/backend/npm-servers/softeria-ms-365-mcp-server/package-lock.json b/backend/npm-servers/softeria-ms-365-mcp-server/package-lock.json index a65e53a3..b5b5f476 100644 --- a/backend/npm-servers/softeria-ms-365-mcp-server/package-lock.json +++ b/backend/npm-servers/softeria-ms-365-mcp-server/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@softeria/ms-365-mcp-server": "^0.75.0" + "@softeria/ms-365-mcp-server": "^0.79.6" } }, "node_modules/@azure-rest/core-client": { @@ -350,9 +350,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.13", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", - "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -412,13 +412,13 @@ } }, "node_modules/@softeria/ms-365-mcp-server": { - "version": "0.75.0", - "resolved": "https://registry.npmjs.org/@softeria/ms-365-mcp-server/-/ms-365-mcp-server-0.75.0.tgz", - "integrity": "sha512-UkrVjbg20Gbaxs4gqhsdSfuAUVHnX8kFKARDhnrEI/KIZicI23CZiCdmK75DkS8ae+n2au+BumK9gI2Utk3Xww==", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@softeria/ms-365-mcp-server/-/ms-365-mcp-server-0.79.6.tgz", + "integrity": "sha512-vr2ea+YRJuinFIqRoqh0p5S4HEwN0F9GpKC8iOHtRM6FhmCx9fth1sW1zJnCbTJjJ/Jrit1wTXYUFuAE2QmcEw==", "license": "MIT", "dependencies": { "@azure/msal-node": "^3.8.0", - "@modelcontextprotocol/sdk": "^1.28.0", + "@modelcontextprotocol/sdk": "^1.29.0", "@toon-format/toon": "^0.8.0", "commander": "^11.1.0", "dotenv": "^17.0.1", @@ -426,7 +426,8 @@ "js-yaml": "^4.1.0", "open": "^11.0.0", "winston": "^3.17.0", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zod-to-json-schema": "^3.25.1" }, "bin": { "ms-365-mcp-server": "dist/index.js" @@ -912,9 +913,9 @@ } }, "node_modules/dotenv": { - "version": "17.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", - "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1284,9 +1285,9 @@ } }, "node_modules/hono": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", - "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", "engines": { "node": ">=16.9.0" diff --git a/backend/npm-servers/softeria-ms-365-mcp-server/package.json b/backend/npm-servers/softeria-ms-365-mcp-server/package.json index afe241d7..23e84819 100644 --- a/backend/npm-servers/softeria-ms-365-mcp-server/package.json +++ b/backend/npm-servers/softeria-ms-365-mcp-server/package.json @@ -11,6 +11,6 @@ "license": "ISC", "type": "commonjs", "dependencies": { - "@softeria/ms-365-mcp-server": "^0.75.0" + "@softeria/ms-365-mcp-server": "^0.79.6" } } diff --git a/debugger/build/lib/debug.py b/debugger/build/lib/debug.py new file mode 100644 index 00000000..bef85d4a --- /dev/null +++ b/debugger/build/lib/debug.py @@ -0,0 +1,57 @@ +import re +import inspect +import os +from debugger_backend.log_config import log_config +from debugger_backend.Debugleton import Debugleton +from debugger_backend.color_adjuster import rgb_to_ansi, bold_and_italicize_text, hex_to_rgb +from debugger_backend.debug_arg_parser import is_text, is_error + +def debug(*args, mode:str='debug', override_max_chars:bool=False): + frame = inspect.currentframe().f_back + code = frame.f_code + line_no = frame.f_lineno + calling_function_name = frame.f_code.co_name + calling_file_name = os.path.basename(code.co_filename) + if calling_function_name == "": + calling_function_name = calling_file_name + # Retrieve the file path of the calling function + file_path = os.path.abspath(code.co_filename) + # print(f"FILE PATH: {file_path}") + t_color, t_is_on, t_emoji = Debugleton().find_file_info(file_path) + # print(f"DEBUGGING: {t_color}, {t_is_on}") + max_chars = 3000 + + with open(code.co_filename, 'r', encoding='utf-8') as f: + lines = f.readlines() + line = lines[line_no - 1] + leading_spaces = len(line) - len(line.lstrip(' ')) + indent = leading_spaces // 4 + arg_names = re.findall(r'debug\((.*?)\)', line)[0].split(', ') + for arg_name, arg_value in zip(arg_names, args): + indent_str = ' |\t' * indent + if indent > 0: + indent_str = indent_str[:-3] + ' |-- ' + arg_is_error = is_error(arg_value, arg_name) + arg_is_text = is_text(arg_value, arg_name) + if arg_is_error: + t_color = "#FE3F3F" + t_emoji = "❌" + t_is_on = True + + arg_len = len(str(arg_value)) + if arg_len > max_chars and not override_max_chars: + if not arg_is_text: arg_value = str(arg_value) + arg_value = arg_value[:int(max_chars/2)] + "...\n..." + arg_value[arg_len-int(max_chars/2):] + + function_print_str = calling_function_name if 'self' not in frame.f_locals else f'{frame.f_locals["self"].__class__.__name__}.{calling_function_name}' + # color = COLORS.get(function_print_str, white) + color = hex_to_rgb(t_color) + if arg_is_text: + print_str = f"{t_emoji}{rgb_to_ansi(color)}{indent_str}[{function_print_str}] : {bold_and_italicize_text(arg_value)}\033[0m" + else: + print_str = f"{t_emoji}{rgb_to_ansi(color)}{indent_str}[{function_print_str}] : {arg_name} = {arg_value}\033[0m" + if t_is_on: log_config.debug_custom(print_str, mode) + +# Assign the function to the module's __call__ attribute +import sys +sys.modules[__name__] = debug diff --git a/debugger/build/lib/debugger_backend/DEFAULTS.py b/debugger/build/lib/debugger_backend/DEFAULTS.py new file mode 100644 index 00000000..129962ab --- /dev/null +++ b/debugger/build/lib/debugger_backend/DEFAULTS.py @@ -0,0 +1,7 @@ +import os +TOGGLE_FILE = os.path.join(os.path.dirname(__file__), 'debug_toggles.json') +DEFAULT_COLOR = '#ffffff' +DEFAULT_TOGGLED = False +DEFAULT_SET_MANUALLY = False +DEFAULT_EMOJI = '⚫' +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) \ No newline at end of file diff --git a/debugger/build/lib/debugger_backend/DebugFile.py b/debugger/build/lib/debugger_backend/DebugFile.py new file mode 100644 index 00000000..870566cf --- /dev/null +++ b/debugger/build/lib/debugger_backend/DebugFile.py @@ -0,0 +1,39 @@ +import os +from debugger_backend.File import File +from debugger_backend.DEFAULTS import DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_SET_MANUALLY, DEFAULT_EMOJI + +class DebugFile(File): + def __init__(self, filename, path, color=DEFAULT_COLOR, is_toggled=DEFAULT_TOGGLED, + set_manually=DEFAULT_SET_MANUALLY, emoji=DEFAULT_EMOJI, directory=None): + super().__init__(filename, path) + self.color = color + self.is_toggled = is_toggled + self.set_manually = set_manually + self.emoji = emoji + self.directory = directory # Reference to parent directory + + def to_dict(self): + """ + Converts the DebugFile object to a dictionary format. + """ + return { + "name": os.path.basename(self.filename), + "color": self.color, + "is_toggled": self.is_toggled, + "set_manually": self.set_manually, + "emoji": self.emoji + } + + @classmethod + def from_dict(cls, file_dict, directory): + """ + Creates a DebugFile object from a dictionary loaded from JSON. + """ + filename = os.path.join(directory.path, file_dict["name"]) + return cls( + filename=filename, + color=file_dict.get("color", DEFAULT_COLOR), + is_toggled=file_dict.get("is_toggled", DEFAULT_TOGGLED), + set_manually=file_dict.get("set_manually", DEFAULT_SET_MANUALLY), + directory=directory + ) diff --git a/debugger/build/lib/debugger_backend/Debugleton.py b/debugger/build/lib/debugger_backend/Debugleton.py new file mode 100644 index 00000000..7c97ae2e --- /dev/null +++ b/debugger/build/lib/debugger_backend/Debugleton.py @@ -0,0 +1,88 @@ +# Haik: sorry bout the filename + +import threading +from debugger_backend.project_scanner import update_debug_toggles +from debugger_backend.Directory import Directory +from debugger_backend.DebugFile import DebugFile +from debugger_backend.DEFAULTS import DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_EMOJI +import os +import time + +NEEDS_RESYNC_FILE = os.path.join(os.path.dirname(__file__), 'needs_resync.txt') + +class Debugleton: + _instance = None + _lock = threading.Lock() # Lock for thread-safe singleton creation + sync_lock: threading.Lock + + def __new__(cls, *args, **kwargs): + # Double-checked locking for thread-safe singleton creation + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = super(Debugleton, cls).__new__(cls) + print("\033[38;5;120m\n---------------------------------\033[0m") + print("\033[38;5;120m|\tDEBUGLETON INIT \t|\033[0m") + cls._instance.dir = None + print("\033[38;5;120m|\tScanning Project...\t|\033[0m") + cls._instance.sync_lock = threading.Lock() + cls._instance.sync_lock.acquire(blocking=False) + cls._instance.sync_to_saved(is_first_sync=True) + cls._instance.sync_lock.release() + print("\033[38;5;120m|\t...Project Scanned\t|\033[0m") + print("\033[38;5;120m|\tDEBUGLETON INIT DONE\t|\033[0m") + print("\033[38;5;120m---------------------------------\n\033[0m") + # else: print("DEBUGLETON Already initialized INNER") + # else: print("DEBUGLETON Already initialized OUTER") + return cls._instance + + + def sync_to_saved(self, is_first_sync=False): + # print(f"[sync_to_saved]: START") + if not is_first_sync: self.sync_lock.acquire() + # print(f"[sync_to_saved]: Acquired sync lock") + self.dir = update_debug_toggles(save_to_file=False) + # print(f"Synced to saved dir: {self.dir}") + self.abspaths, self.instances = self.dir.get_ordered_abspaths_and_instances() + # print(f"Synced to abspaths: {self.abspaths}") + with open(NEEDS_RESYNC_FILE, 'w') as f: + f.write('0') + if not is_first_sync: self.sync_lock.release() + # print(f"[sync_to_saved]: Released sync lock") + # print(f"[sync_to_saved]: END") + + def needs_resync(self): + # print(f"[needs_resync]: START") + num_tries = 0 + while self.is_syncing(): + print(f"Waiting for Debugleton to sync... ({num_tries})") + time.sleep(5) + num_tries += 1 + if num_tries > 10: + print(f""" + NOTE: Debugleton is taking a long time, there's one scenario where it breaks: + \n\t- If running in docker, and you deleted one of the root dirs in the volumes of docker compose, + \n\t then the debugger will not be able to find the project and will get stuck in an infinite loop. + \n\t- In this case, you can restart the docker container and delete the volume in the docker compose file and it will resync. + """) + with open(NEEDS_RESYNC_FILE, 'r') as f: + does_need_resync = True if f.read().strip() == '1' else False + # if does_need_resync: print("Resyncing Debugleton...") + # print(f"[needs_resync]: END") + return does_need_resync + + def is_syncing(self): + return self.sync_lock.locked() + + def find_file_info(self, filepath: str): + filepath = filepath.lower() + # print(f"Finding file info for {filepath}") + if self.needs_resync(): + self.sync_to_saved() + try: + filepath_id = self.abspaths.index(filepath) + match = self.instances[filepath_id] + return match.color, match.is_toggled, match.emoji + except ValueError: + print(f"Filepath not found: {filepath}") + return DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_EMOJI \ No newline at end of file diff --git a/debugger/build/lib/debugger_backend/Directory.py b/debugger/build/lib/debugger_backend/Directory.py new file mode 100644 index 00000000..fe23db47 --- /dev/null +++ b/debugger/build/lib/debugger_backend/Directory.py @@ -0,0 +1,200 @@ +import os +import json +import colorsys +from pathlib import Path +from debugger_backend.DebugFile import DebugFile +from debugger_backend.DEFAULTS import DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_SET_MANUALLY, DEFAULT_EMOJI +from debugger_backend.path_mngr import get_abspath, get_root_rel_path + +class Directory: + def __init__(self, path, color=DEFAULT_COLOR, is_toggled=DEFAULT_TOGGLED, + set_manually=DEFAULT_SET_MANUALLY, emoji=DEFAULT_EMOJI): + self.path = path + # print(f"Directory init: {self.path}") + self.children = [] # Can contain DebugFile or other Directory objects + self.color = color + self.is_toggled = is_toggled + self.set_manually = set_manually + self.emoji = emoji + + def __str__(self): + return f"Directory: {self.path}\nNum Children: {len(self.children)}\nColor: {self.color}\nToggled: {self.is_toggled}\nSet Manually: {self.set_manually}" + + def get_abspath(self): + return get_abspath(self.path) + + def add_child(self, child): + """ + Adds a child to the directory (either a DebugFile or another Directory). + """ + self.children.append(child) + + def get_ordered_abspaths_and_instances(self): + # print("[get_ordered_abspaths]: START") + curr_file_path = os.path.abspath(__file__) + root_dir = os.path.dirname(os.path.dirname(os.path.dirname(curr_file_path))) + # print(f"[get_ordered_abspaths]: Curr path: {curr_file_path}") + # print(f"[get_ordered_abspaths]: Dir path: {root_dir}") + def construct_ordered_abspaths(dir: Directory, ordered_abspaths: list): + dir_path = dir.path + full_path = os.path.join(root_dir, dir_path) + ordered_abspaths.append({"abspath": full_path, "instance": dir}) + # print(f"\t[construct_ordered_abspaths]: Full path: {full_path}") + for child in dir.children: + child_abspath = os.path.join(root_dir, child.path).lower() + if os.path.isdir(child_abspath): + construct_ordered_abspaths(child, ordered_abspaths) + elif os.path.isfile(child_abspath): + # print(f"\t[construct_ordered_abspaths]: Child is file: {child_abspath}") + ordered_abspaths.append({"abspath": child_abspath, "instance": child}) + else: + print(f"\033[38;5;120mEntry is non existent: {child_abspath}\033[0m") + # print(f"\t[construct_ordered_abspaths]: Finished for dir: {full_path}") + # print(f"\t[construct_ordered_abspaths]: RETURNING FROM DIR: {full_path}") + return ordered_abspaths + ordered_abspaths_and_instances = construct_ordered_abspaths(self, []) + # print("[get_ordered_abspaths]: Finished getting ordered abspaths and instances") + # for abspath_and_instance in ordered_abspaths_and_instances: + # abspath = abspath_and_instance["abspath"] + # print(f"\t[get_ordered_abspaths]: Abspath: {abspath}") + ordered_abspaths = [abspath_and_instance["abspath"] for abspath_and_instance in ordered_abspaths_and_instances] + ordered_instances = [abspath_and_instance["instance"] for abspath_and_instance in ordered_abspaths_and_instances] + return ordered_abspaths, ordered_instances + + + def build_structure(self): + print("[build_structure]: START") + root_dir = self.get_abspath() + # print(f"[build_structure]: Root dir: {root_dir}") + excluded_dirs = [".venv", "debugger", "node_modules", ".git", "__pycache__"] + project_structure = [] + + def construct_project_structure(dir_path: str, parent_dir: Directory): + # print(f"[build_structure]: Scanning dir: {dir_path}") + with os.scandir(dir_path) as it: + for entry in it: + # print(f"[build_structure]: Entry: {entry.path}") + if any(excluded_dir in entry.path for excluded_dir in excluded_dirs): + # print(f"[build_structure]: Excluding {entry.path}") + continue + root_rel_path = get_root_rel_path(entry.path) + if entry.is_dir(): + subdir = Directory(root_rel_path) + construct_project_structure(entry.path, subdir) + parent_dir.add_child(subdir) + elif entry.is_file(): + debug_file = DebugFile(filename=entry.name, path=root_rel_path) + if debug_file.calls_debug_function(): + parent_dir.add_child(debug_file) + else: + continue + + construct_project_structure(root_dir, self) + # [print(f"[build_structure]: {file}") for file in project_structure] + # print(f"[build_structure]: END") + return + + def to_dict(self): + """ + Converts the Directory object to a dictionary format, recursively. + """ + return { + "name": os.path.basename(self.path), + "color": self.color, + "is_toggled": self.is_toggled, + "set_manually": self.set_manually, + "emoji": self.emoji, + "children": [child.to_dict() if isinstance(child, DebugFile) else child.to_dict() for child in self.children] + } + + def prune_empty(self): + # Recursively prune empty directories + # Base case) if the current directory has no children, return + # Recursive case) for each of the directories in the current directory, call prune_empty + # then remove the directory from the children of the current directory if it has no children + for child in self.children[:]: + if isinstance(child, Directory): + # Recursively prune empty subdirectories + child.prune_empty() + # If the subdirectory is empty after pruning, remove it + if len(child.children) == 0: + self.children.remove(child) + + def propagate_toggled_state(self): + """ + Propagates the toggled state down the hierarchy. + """ + for child in self.children: + if isinstance(child, DebugFile) and not child.set_manually: + child.is_toggled = self.is_toggled + elif isinstance(child, Directory) and not child.set_manually: + child.is_toggled = self.is_toggled + child.propagate_toggled_state() + + def propagate_color(self, parent_color=DEFAULT_COLOR): + """ + Propagates the color from parent to children. + """ + if self.color == DEFAULT_COLOR: + self.color = lighten_color(parent_color) + for child in self.children: + if isinstance(child, DebugFile) and child.color == DEFAULT_COLOR: + child.color = lighten_color(self.color) + elif isinstance(child, Directory): + child.propagate_color(self.color) + + def load_from_json(self, json_data): + """ + Loads a directory structure from a JSON file into this Directory instance. + """ + for item in json_data: + if 'children' in item: + subdir = Directory( + path=os.path.join(self.path, item['name']), + color=item.get('color', DEFAULT_COLOR), + is_toggled=item.get('is_toggled', DEFAULT_TOGGLED), + set_manually=item.get('set_manually', DEFAULT_SET_MANUALLY), + emoji=item.get('emoji', DEFAULT_EMOJI) + ) + + subdir.load_from_json(item['children']) + self.add_child(subdir) + else: + # debug_file = DebugFile.from_dict(item, self) + debug_file = DebugFile( + filename=item['name'], + path=os.path.join(self.path, item['name']), + color=item.get('color', DEFAULT_COLOR), + is_toggled=item.get('is_toggled', DEFAULT_TOGGLED), + set_manually=item.get('set_manually', DEFAULT_SET_MANUALLY), + emoji=item.get('emoji', DEFAULT_EMOJI), + directory=self + ) + self.add_child(debug_file) + + def reset_colors(self): + """ + Resets the color of all DebugFile and Directory objects in this directory structure to the default color. + """ + self.color = DEFAULT_COLOR + for child in self.children: + if isinstance(child, DebugFile): + child.color = DEFAULT_COLOR + elif isinstance(child, Directory): + child.reset_colors() + + +def lighten_color(color, amount=0.1): + """ + Lightens the given color by the specified amount. + """ + try: + color = color.lstrip('#') + r, g, b = int(color[:2], 16), int(color[2:4], 16), int(color[4:6], 16) + h, l, s = colorsys.rgb_to_hls(r / 255.0, g / 255.0, b / 255.0) + l = min(1, l + amount) + r, g, b = colorsys.hls_to_rgb(h, l, s) + return '#{:02x}{:02x}{:02x}'.format(int(r * 255), int(g * 255), int(b * 255)) + except Exception as e: + print(f"Error lightening color {color}: {e}") + return color diff --git a/debugger/build/lib/debugger_backend/File.py b/debugger/build/lib/debugger_backend/File.py new file mode 100644 index 00000000..f785a9eb --- /dev/null +++ b/debugger/build/lib/debugger_backend/File.py @@ -0,0 +1,29 @@ +import os +from debugger_backend.path_mngr import get_abspath + +class File: + def __init__(self, filename, path): + self.filename = filename + self.path = path + + def get_abspath(self): + return get_abspath(self.path) + + def calls_debug_function(self): + """ + Checks if the file calls the debug function. + """ + full_path = self.get_abspath() + + if not full_path.endswith('.py') or full_path.endswith('.pyc'): + result = False + else: + try: + with open(full_path, 'r', encoding='utf-8') as file: + content = file.read() + result = 'debug(' in content + except (UnicodeDecodeError, FileNotFoundError) as e: + print(f"Error reading file {full_path}") + result = False + # print(f"??calls_debug_function?? {result}") + return result \ No newline at end of file diff --git a/debugger/build/lib/debugger_backend/__init__.py b/debugger/build/lib/debugger_backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/debugger/build/lib/debugger_backend/color_adjuster.py b/debugger/build/lib/debugger_backend/color_adjuster.py new file mode 100644 index 00000000..de7abf22 --- /dev/null +++ b/debugger/build/lib/debugger_backend/color_adjuster.py @@ -0,0 +1,21 @@ +import colorsys + +def adjust_brightness(color, brightness_factor): + hls = colorsys.rgb_to_hls(*[x/255.0 for x in color]) # Convert RGB to HLS + hls = (hls[0], max(0, min(1, hls[1] + brightness_factor)), hls[2]) # Adjust lightness + rgb = [int(x*255.0) for x in colorsys.hls_to_rgb(*hls)] # Convert back to RGB + return rgb + + +def rgb_to_ansi(rgb): + return '\033[38;2;{};{};{}m'.format(*rgb) + +def bold_and_italicize_text(text): + return f"\033[1m\033[3m{text}\033[0m" + +def hex_to_rgb(hex_code): + # Remove the '#' symbol if it exists + hex_code = hex_code.lstrip('#') + + # Convert the hex code to RGB + return tuple(int(hex_code[i:i+2], 16) for i in (0, 2, 4)) \ No newline at end of file diff --git a/debugger/build/lib/debugger_backend/debug_arg_parser.py b/debugger/build/lib/debugger_backend/debug_arg_parser.py new file mode 100644 index 00000000..fcabb1f0 --- /dev/null +++ b/debugger/build/lib/debugger_backend/debug_arg_parser.py @@ -0,0 +1,21 @@ + +def is_fstring(arg_name): + if not isinstance(arg_name, str): + return False + # print(f"arg_name: {arg_name}") + fstring_start_values = ["f'", "f\""] + num_start_matches = sum(arg_name.startswith(start_value) for start_value in fstring_start_values) + conditions = [num_start_matches == 1] + return all(conditions) + +def is_text(arg_value, arg_name): + arg_is_text = isinstance(arg_value, str) and len(arg_name) > 2 and arg_name[1:len(arg_name)-1] == arg_value and not arg_name.endswith(")") + if not arg_is_text: + arg_is_text = is_fstring(arg_name) + # print(f"is_text: {arg_is_text}") + return arg_is_text + +def is_error(arg_value, arg_name): + arg_is_error = isinstance(arg_value, Exception) or "error" in str(arg_value).lower() or "error" in str(arg_name).lower() + return arg_is_error + diff --git a/debugger/build/lib/debugger_backend/debugger_server.py b/debugger/build/lib/debugger_backend/debugger_server.py new file mode 100644 index 00000000..12277dc5 --- /dev/null +++ b/debugger/build/lib/debugger_backend/debugger_server.py @@ -0,0 +1,49 @@ +from flask import Flask, request, jsonify, Response +from flask_cors import CORS +from debugger_backend.project_scanner import update_debug_toggles, dir_to_output_format +import json +import os +NEEDS_RESYNC_FILE = os.path.join(os.path.dirname(__file__), 'needs_resync.txt') +DEBUG_TOGGLE_FILE = os.path.join(os.path.dirname(__file__), 'debug_toggles.json') +app = Flask(__name__) +CORS(app) + +@app.route('/pull_structure', methods=['GET']) +def api_get_structure(): + print("GET /get_structure") + scanned_dir=update_debug_toggles(save_to_file=True) + # print("\n\nPS scanned_dir: ", scanned_dir) + output = dir_to_output_format(scanned_dir) + output = json.dumps(output, ensure_ascii=False, indent=4) + # print("output: ", output) + return Response(output, mimetype='application/json') + +@app.route('/push_structure', methods=['POST']) +def api_push_structure(): + print("POST /push_structure") + data = request.get_json() + data = data['projectStructure'] + # print(data) + with open(DEBUG_TOGGLE_FILE, 'w', encoding='utf-8') as file: + json.dump(data, file, indent=4) + with open(NEEDS_RESYNC_FILE, 'w') as f: + f.write('1') + return jsonify({"status": "success"}) + +@app.route('/reset_color', methods=['POST']) +def api_reset_color(): + print("POST /reset_color") + scanned_dir=update_debug_toggles(save_to_file=False) + scanned_dir.reset_colors() + # print("RS: scanned_dir: ", scanned_dir) + output = dir_to_output_format(scanned_dir) + output = json.dumps(output, ensure_ascii=False, indent=4) + # print("RS: output: ", output) + return Response(output, mimetype='application/json') + + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=6969, debug=False) + + diff --git a/debugger/build/lib/debugger_backend/log_config.py b/debugger/build/lib/debugger_backend/log_config.py new file mode 100644 index 00000000..19a3b14a --- /dev/null +++ b/debugger/build/lib/debugger_backend/log_config.py @@ -0,0 +1,50 @@ +import logging +from debugger_backend.log_mode import get_log_mode, set_log_mode + +class LogConfig: + _instance = None + MODES = { + "all": 1, + "debug": 10, + "test": 20, + } + + def __new__(cls): + if cls._instance is None: + cls._instance = super(LogConfig, cls).__new__(cls) + cls._instance._initialize_logger() + return cls._instance + + def _initialize_logger(self): + for name, level in self.MODES.items(): + logging.addLevelName(level, name.upper()) + self.logger = logging.getLogger('custom_logger') + self.logger.propagate = False # Prevent log propagation + handler = logging.StreamHandler() + formatter = logging.Formatter('%(message)s') + handler.setFormatter(formatter) + + # Remove existing handlers to prevent duplicate logging + if self.logger.hasHandlers(): + self.logger.handlers.clear() + + self.logger.addHandler(handler) + self.set_debug_mode(get_log_mode()) + + def debug_custom(self, message, mode = None, *args, **kwargs): + if mode is None: + mode = get_log_mode() + if self.logger.isEnabledFor(self.MODES[mode]): + self.logger._log(self.MODES[mode], message, args, **kwargs) + + def set_debug_mode(self, mode): + current_mode = get_log_mode() + # print(f"Setting debug mode from {current_mode} -> to {mode}") + if mode not in self.MODES: raise ValueError(f"Invalid mode: {mode}") + set_log_mode(mode) + self.logger.setLevel(self.MODES[mode]) + + def get_debug_mode(self): + return get_log_mode() + +log_config = LogConfig() diff --git a/debugger/build/lib/debugger_backend/log_mode.py b/debugger/build/lib/debugger_backend/log_mode.py new file mode 100644 index 00000000..e1051782 --- /dev/null +++ b/debugger/build/lib/debugger_backend/log_mode.py @@ -0,0 +1,13 @@ +import os + +# LOG_MODE_FILE = 'debugger/log_mode.txt' +LOG_MODE_FILE = os.path.join(os.path.dirname(__file__), 'log_mode.txt') +def set_log_mode(mode): + with open(LOG_MODE_FILE, 'w') as f: + f.write(mode) + +def get_log_mode(): + if os.path.exists(LOG_MODE_FILE): + with open(LOG_MODE_FILE, 'r') as f: + return f.read().strip() + return 'all' # Default to 'all' if the file doesn't exist diff --git a/debugger/build/lib/debugger_backend/path_mngr.py b/debugger/build/lib/debugger_backend/path_mngr.py new file mode 100644 index 00000000..0b47d49a --- /dev/null +++ b/debugger/build/lib/debugger_backend/path_mngr.py @@ -0,0 +1,12 @@ +import os +from debugger_backend.DEFAULTS import ROOT_DIR + +def get_abspath(path: str): + return os.path.join(ROOT_DIR, path) + +def get_root_rel_path(path: str): + assert path.startswith(ROOT_DIR) + path = path[len(ROOT_DIR):] + while path.startswith(os.sep): + path = path[1:] + return path diff --git a/debugger/build/lib/debugger_backend/project_scanner.py b/debugger/build/lib/debugger_backend/project_scanner.py new file mode 100644 index 00000000..81a93df6 --- /dev/null +++ b/debugger/build/lib/debugger_backend/project_scanner.py @@ -0,0 +1,149 @@ +import os +import json +import colorsys +from typing import Union +from debugger_backend.Directory import Directory +from debugger_backend.DEFAULTS import DEFAULT_COLOR, DEFAULT_TOGGLED, DEFAULT_SET_MANUALLY, TOGGLE_FILE, DEFAULT_EMOJI, ROOT_DIR +from debugger_backend.DebugFile import DebugFile +from collections import OrderedDict + +def merge_directories(json_dir: Directory, scanned_dir: Directory): + """ + Merges two Directory instances: one loaded from JSON (json_dir) and one built from scanning (scanned_dir). + The values from json_dir take precedence where attributes overlap. + It matches based on full directory and file structure, not just file names. + """ + # print(f"Merging JSON_DIR: {json_dir.path}\n with SCAN_DIR: {scanned_dir.path}") + json_abspaths, json_instances = json_dir.get_ordered_abspaths_and_instances() + # print(f"json_abspaths: {json_abspaths}") + scanned_abspaths, scanned_instances = scanned_dir.get_ordered_abspaths_and_instances() + # print(f"scanned_abspaths: {scanned_abspaths}") + + def find_matching_in_structure(scanned_child: Union[DebugFile, Directory], json_dir: Directory): + assert json_dir in json_instances, f"JSON_DIR: {json_dir.path} not in json_instances" + assert scanned_child in scanned_instances, f"SCANNED_CHILD: {scanned_child.path} not in scanned_instances" + scanned_id = scanned_instances.index(scanned_child) + scanned_abspath = scanned_abspaths[scanned_id] + json_instance = None + try: + json_id = json_abspaths.index(scanned_abspath) + json_instance = json_instances[json_id] + # print(f"Match found: {scanned_child.path} == {json_instance.path}") + except ValueError: + # print(f"SCANNED_ABSPATH: {scanned_abspath} not in JSON_ABSPATHS") + pass + return json_instance + + def construct_merged_dir(json_dir: Directory, scanned_dir: Directory): + for scanned_child in scanned_dir.children: + # Use the new recursive function to find the corresponding child in the JSON directory structure + matching_json_child = find_matching_in_structure(scanned_child, json_dir) + + if isinstance(scanned_child, DebugFile) and matching_json_child: + # Merge attributes from the JSON-loaded structure + scanned_child.color = matching_json_child.color + scanned_child.is_toggled = matching_json_child.is_toggled + scanned_child.set_manually = matching_json_child.set_manually + scanned_child.emoji = matching_json_child.emoji + + elif isinstance(scanned_child, Directory) and matching_json_child: + # Merge directory attributes + scanned_child.color = matching_json_child.color + scanned_child.is_toggled = matching_json_child.is_toggled + scanned_child.set_manually = matching_json_child.set_manually + scanned_child.emoji = matching_json_child.emoji + + # Recursively merge the subdirectories + construct_merged_dir(matching_json_child, scanned_child) + else: + scanned_child.color = DEFAULT_COLOR + scanned_child.is_toggled = DEFAULT_TOGGLED + scanned_child.set_manually = DEFAULT_SET_MANUALLY + scanned_child.emoji = DEFAULT_EMOJI + + construct_merged_dir(json_dir, scanned_dir) + + +def update_debug_toggles(save_to_file=True) -> Directory: + # print(f"[update_debug_toggles]: START") + json_loaded_dir = None + if os.path.exists(TOGGLE_FILE): + with open(TOGGLE_FILE, 'r', encoding='utf-8') as file: + try: + json_data = json.load(file) + json_loaded_dir = Directory("") + json_loaded_dir = Directory(path="", + color=json_data[0].get('color', DEFAULT_COLOR), + is_toggled=json_data[0].get('is_toggled', DEFAULT_TOGGLED), + set_manually=json_data[0].get('set_manually', DEFAULT_SET_MANUALLY), + emoji=json_data[0].get('emoji', DEFAULT_EMOJI) + ) + # print(f"Root: {json_loaded_dir}") + # print("Json Children 1:") + # [print(child.path) for child in json_loaded_dir.children] + + json_loaded_dir.load_from_json(json_data[0]['children']) # Assuming the root is in json_data[0] + # print("Json Children 2:") + # [print(child.path) for child in json_loaded_dir.children] + + except json.JSONDecodeError: + ValueError("Error: JSON file could not be decoded.") + else: + print("No JSON file found") + # 1. Create a directory structure from the filesystem scan + # print("Scanning directory...") + scanned_dir = Directory(path="", + color=json_loaded_dir.color if json_loaded_dir else DEFAULT_COLOR, + is_toggled=json_loaded_dir.is_toggled if json_loaded_dir else DEFAULT_TOGGLED, + set_manually=json_loaded_dir.set_manually if json_loaded_dir else DEFAULT_SET_MANUALLY, + emoji=json_loaded_dir.emoji if json_loaded_dir else DEFAULT_EMOJI + ) + # print(f"\n\nNum Children 1: {len(scanned_dir.children)}") + # [print(child.path) for child in scanned_dir.children] + scanned_dir.build_structure() + # print(f"\n\nNum Children 2: {len(scanned_dir.children)}") + # [print(child.path) for child in scanned_dir.children] + scanned_dir.prune_empty() + # print(f"\n\nNum Children 3: {len(scanned_dir.children)}") + # [print(child.path) for child in scanned_dir.children] + + # print("1.1 Merged Dir First Child: ", scanned_dir.children[0]) + # 4. Propagate the toggled state and color through the merged structure + scanned_dir.propagate_toggled_state() + # print(f"\n\nNum Children 4: {len(scanned_dir.children)}") + # [print(child.path) for child in scanned_dir.children] + + # 3. Merge the two directory structures + if json_loaded_dir: + merge_directories(json_loaded_dir, scanned_dir) + + # print(f"\n\nNum Children 5: {len(scanned_dir.children)}") + scanned_dir.propagate_color() + output = dir_to_output_format(scanned_dir) + # print(f"\n\nNum Children 6: {len(scanned_dir.children)}") + + + # 5. Write the updated structure back to the JSON file + if save_to_file: + with open(TOGGLE_FILE, 'w', encoding='utf-8') as file: + json.dump(output, file, ensure_ascii=False, indent=4) + # print(f"[update_debug_toggles]: END") + return scanned_dir + +def dir_to_output_format(input_dir): + root_node = { + "name": "root", + "color": input_dir.color, # Use input_dir's color + "is_toggled": input_dir.is_toggled, # Use input_dir's toggled state + "set_manually": input_dir.set_manually, # Use input_dir's set_manually + "emoji": input_dir.emoji, # Use input_dir's emoji + "children": input_dir.to_dict()["children"] + } + return [ordered(root_node)] + +def ordered(obj): + if isinstance(obj, dict): + return OrderedDict((k, ordered(v)) for k, v in obj.items()) + if isinstance(obj, list): + return [ordered(x) for x in obj] + return obj diff --git a/electron/package.json b/electron/package.json index 04ee03eb..b70de317 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "openswarm", - "version": "1.0.22", + "version": "1.0.23", "description": "OpenSwarm — AI Agent Orchestrator", "main": "main.js", "scripts": { @@ -44,7 +44,7 @@ }, "dmg": { "artifactName": "OpenSwarm-${arch}.${ext}", - "title": "OpenSwarm", + "title": "OpenSwarm ${version}", "contents": [ { "x": 130, diff --git a/scripts/build-python-env.sh b/scripts/build-python-env.sh index af321c58..279c604a 100755 --- a/scripts/build-python-env.sh +++ b/scripts/build-python-env.sh @@ -86,7 +86,7 @@ echo "Installing backend dependencies..." # Install the debugger module echo "Installing debugger module..." -"$PYTHON_BIN" -m pip install -e "$PROJECT_ROOT/debugger" +"$PYTHON_BIN" -m pip install "$PROJECT_ROOT/debugger" # Verify claude-agent-sdk and its bundled binary echo "Verifying claude-agent-sdk..."