@aliwey/bmo 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +90 -0
  2. package/bin/bmo.js +188 -0
  3. package/cli.py +1129 -0
  4. package/config/__init__.py +0 -0
  5. package/config/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/config/__pycache__/settings.cpython-313.pyc +0 -0
  7. package/config/__pycache__/system-prompt.cpython-313.pyc +0 -0
  8. package/config/settings.py +104 -0
  9. package/config/system-prompt.json +18 -0
  10. package/core/__init__.py +0 -0
  11. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  12. package/core/__pycache__/bfp_a2a_bridge.cpython-313.pyc +0 -0
  13. package/core/__pycache__/bfp_agent.cpython-313.pyc +0 -0
  14. package/core/__pycache__/bfp_agent_card.cpython-313.pyc +0 -0
  15. package/core/__pycache__/bfp_connector.cpython-313.pyc +0 -0
  16. package/core/__pycache__/bfp_discovery.cpython-313.pyc +0 -0
  17. package/core/__pycache__/bfp_identity.cpython-313.pyc +0 -0
  18. package/core/__pycache__/bfp_tasks.cpython-313.pyc +0 -0
  19. package/core/__pycache__/bfp_transport.cpython-313.pyc +0 -0
  20. package/core/__pycache__/bmo_engine.cpython-313.pyc +0 -0
  21. package/core/__pycache__/bot_client.cpython-313.pyc +0 -0
  22. package/core/__pycache__/budget_tracker.cpython-313.pyc +0 -0
  23. package/core/__pycache__/cli_renderer.cpython-313.pyc +0 -0
  24. package/core/__pycache__/goal_runner.cpython-313.pyc +0 -0
  25. package/core/__pycache__/request_worker.cpython-313.pyc +0 -0
  26. package/core/__pycache__/security.cpython-313.pyc +0 -0
  27. package/core/__pycache__/shared_state.cpython-313.pyc +0 -0
  28. package/core/__pycache__/worker_manager.cpython-313.pyc +0 -0
  29. package/core/__pycache__/worker_multiproc.cpython-313.pyc +0 -0
  30. package/core/__pycache__/worker_protocol.cpython-313.pyc +0 -0
  31. package/core/__pycache__/worker_subprocess.cpython-313.pyc +0 -0
  32. package/core/bfp_a2a_bridge.py +399 -0
  33. package/core/bfp_agent.py +98 -0
  34. package/core/bfp_agent_card.py +161 -0
  35. package/core/bfp_connector.py +177 -0
  36. package/core/bfp_discovery.py +105 -0
  37. package/core/bfp_identity.py +83 -0
  38. package/core/bfp_tasks.py +70 -0
  39. package/core/bfp_transport.py +368 -0
  40. package/core/bmo_engine.py +405 -0
  41. package/core/bot_client.py +838 -0
  42. package/core/budget_tracker.py +62 -0
  43. package/core/cli_renderer.py +177 -0
  44. package/core/goal_runner.py +129 -0
  45. package/core/request_worker.py +242 -0
  46. package/core/security.py +42 -0
  47. package/core/shared_state.py +4 -0
  48. package/core/worker_manager.py +71 -0
  49. package/core/worker_multiproc.py +155 -0
  50. package/core/worker_protocol.py +30 -0
  51. package/core/worker_subprocess.py +222 -0
  52. package/handlers/__init__.py +0 -0
  53. package/handlers/__pycache__/__init__.cpython-313.pyc +0 -0
  54. package/handlers/__pycache__/messages.cpython-313.pyc +0 -0
  55. package/handlers/messages.py +2761 -0
  56. package/main.py +125 -0
  57. package/memory.md +43 -0
  58. package/models/__init__.py +0 -0
  59. package/models/__pycache__/__init__.cpython-313.pyc +0 -0
  60. package/models/__pycache__/chat_models.cpython-313.pyc +0 -0
  61. package/models/chat_models.py +143 -0
  62. package/package.json +50 -0
  63. package/registry/worker.js +108 -0
  64. package/registry/wrangler.toml +11 -0
  65. package/requirements.txt +13 -0
  66. package/scripts/bmo_init.js +115 -0
  67. package/scripts/postinstall.js +265 -0
  68. package/scripts/relay_cmd.js +276 -0
  69. package/scripts/web_cmd.js +136 -0
  70. package/setup.py +26 -0
  71. package/storage/__init__.py +0 -0
  72. package/storage/__pycache__/__init__.cpython-313.pyc +0 -0
  73. package/storage/__pycache__/sqlite_storage.cpython-313.pyc +0 -0
  74. package/storage/__pycache__/storage.cpython-313.pyc +0 -0
  75. package/storage/sqlite_storage.py +658 -0
  76. package/storage/storage.py +265 -0
  77. package/tools/__pycache__/bfp_relay.cpython-313.pyc +0 -0
  78. package/tools/__pycache__/get_session_summaries.cpython-313.pyc +0 -0
  79. package/tools/__pycache__/mcp_bridge.cpython-313.pyc +0 -0
  80. package/tools/__pycache__/mcp_server.cpython-313.pyc +0 -0
  81. package/tools/__pycache__/run_mcp_standalone.cpython-313.pyc +0 -0
  82. package/tools/__pycache__/task_registry.cpython-313.pyc +0 -0
  83. package/tools/__pycache__/test_mcp_connection.cpython-313.pyc +0 -0
  84. package/tools/bfp_relay.py +359 -0
  85. package/tools/bot.db +0 -0
  86. package/tools/get_session_summaries.py +45 -0
  87. package/tools/mcp_bridge.py +109 -0
  88. package/tools/mcp_server.py +531 -0
  89. package/tools/register_mcp_task.py +20 -0
  90. package/tools/run_detached.bat +32 -0
  91. package/tools/run_mcp_standalone.py +16 -0
  92. package/tools/task_registry.py +184 -0
  93. package/tools/test_mcp_connection.py +80 -0
  94. package/webchat/package-lock.json +1528 -0
  95. package/webchat/package.json +12 -0
  96. package/webchat/public/app.js +1293 -0
  97. package/webchat/public/index.html +226 -0
  98. package/webchat/public/index.html.bak +416 -0
  99. package/webchat/public/styles.css +2435 -0
  100. package/webchat/server.js +645 -0
@@ -0,0 +1,16 @@
1
+ """Run just the MCP SSE server standalone (without Telegram bot)."""
2
+ import logging
3
+ import os
4
+ import sys
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env"))
8
+
9
+ logging.basicConfig(level=logging.INFO)
10
+
11
+ import uvicorn
12
+ from tools.mcp_server import mcp
13
+
14
+ print("Starting BMO MCP Server on port 4097 (standalone)...")
15
+ print(f"TELEGRAM_BOT_TOKEN: {'SET' if os.getenv('TELEGRAM_BOT_TOKEN') or os.getenv('TELEGRAM_TOKEN') else 'NOT SET'}")
16
+ uvicorn.run(mcp.sse_app, host="127.0.0.1", port=4097, log_level="info")
@@ -0,0 +1,184 @@
1
+ """
2
+ Background Task Registry — tracks all running background tasks (servers, tunnels, etc.)
3
+ Auto-cleans dead PIDs on read. Thread-safe with file locking.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import time
9
+ import psutil
10
+ import threading
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+
14
+ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
15
+ REGISTRY_PATH = os.path.join(PROJECT_ROOT, "data", "background_tasks.json")
16
+ _lock = threading.Lock()
17
+
18
+
19
+ def _ensure_registry():
20
+ """Create registry file if it doesn't exist."""
21
+ if not os.path.exists(REGISTRY_PATH):
22
+ os.makedirs(os.path.dirname(REGISTRY_PATH), exist_ok=True)
23
+ with open(REGISTRY_PATH, "w", encoding="utf-8") as f:
24
+ json.dump({"tasks": []}, f, indent=2)
25
+
26
+
27
+ def _is_process_alive(pid: int) -> bool:
28
+ """Check if a process with the given PID is still running."""
29
+ try:
30
+ proc = psutil.Process(pid)
31
+ return proc.is_running() and proc.status() != psutil.STATUS_ZOMBIE
32
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
33
+ return False
34
+
35
+
36
+ def _cleanup_dead_tasks(tasks: list) -> list:
37
+ """Mark dead PIDs as stopped. Returns cleaned list."""
38
+ cleaned = []
39
+ for task in tasks:
40
+ if task.get("status") == "running" and not _is_process_alive(task["pid"]):
41
+ task["status"] = "stopped"
42
+ task["stopped_at"] = datetime.now().isoformat()
43
+ task["stop_reason"] = "process_terminated"
44
+ cleaned.append(task)
45
+ return cleaned
46
+
47
+
48
+ def read_registry() -> dict:
49
+ """Read the task registry, auto-cleaning dead PIDs."""
50
+ with _lock:
51
+ _ensure_registry()
52
+ try:
53
+ with open(REGISTRY_PATH, "r", encoding="utf-8") as f:
54
+ data = json.load(f)
55
+ except (json.JSONDecodeError, IOError):
56
+ data = {"tasks": []}
57
+
58
+ data["tasks"] = _cleanup_dead_tasks(data.get("tasks", []))
59
+ _save_unlocked(data)
60
+ return data
61
+
62
+
63
+ def _save_unlocked(data: dict):
64
+ """Save registry without acquiring lock (caller must hold lock)."""
65
+ with open(REGISTRY_PATH, "w", encoding="utf-8") as f:
66
+ json.dump(data, f, indent=2, ensure_ascii=False)
67
+
68
+
69
+ def write_registry(data: dict):
70
+ """Write the task registry (thread-safe)."""
71
+ with _lock:
72
+ _ensure_registry()
73
+ data["tasks"] = _cleanup_dead_tasks(data.get("tasks", []))
74
+ _save_unlocked(data)
75
+
76
+
77
+ def register_task(pid: int, command: str, port: int = None, task_type: str = "server",
78
+ description: str = "", log_file: str = "") -> dict:
79
+ """Register a new background task."""
80
+ data = read_registry()
81
+ task = {
82
+ "pid": pid,
83
+ "command": command,
84
+ "port": port,
85
+ "type": task_type,
86
+ "status": "running",
87
+ "url": "",
88
+ "log_file": log_file,
89
+ "start_time": datetime.now().isoformat(),
90
+ "description": description,
91
+ }
92
+ data["tasks"].append(task)
93
+ write_registry(data)
94
+ return task
95
+
96
+
97
+ def update_task(pid: int, **kwargs) -> bool:
98
+ """Update fields of an existing task by PID."""
99
+ data = read_registry()
100
+ for task in data["tasks"]:
101
+ if task["pid"] == pid:
102
+ task.update(kwargs)
103
+ write_registry(data)
104
+ return True
105
+ return False
106
+
107
+
108
+ def remove_task(pid: int) -> bool:
109
+ """Remove a task from the registry."""
110
+ data = read_registry()
111
+ original_len = len(data["tasks"])
112
+ data["tasks"] = [t for t in data["tasks"] if t["pid"] != pid]
113
+ if len(data["tasks"]) < original_len:
114
+ write_registry(data)
115
+ return True
116
+ return False
117
+
118
+
119
+ def get_task(pid: int) -> dict | None:
120
+ """Get a single task by PID."""
121
+ data = read_registry()
122
+ for task in data["tasks"]:
123
+ if task["pid"] == pid:
124
+ return task
125
+ return None
126
+
127
+
128
+ def get_active_tasks() -> list:
129
+ """Get all running tasks (auto-cleans dead ones)."""
130
+ data = read_registry()
131
+ return [t for t in data["tasks"] if t.get("status") == "running"]
132
+
133
+
134
+ def get_tasks_by_port(port: int) -> list:
135
+ """Get all tasks using a specific port."""
136
+ data = read_registry()
137
+ return [t for t in data["tasks"] if t.get("port") == port]
138
+
139
+
140
+ def get_used_ports() -> list:
141
+ """Get all ports currently in use by running tasks."""
142
+ data = read_registry()
143
+ return [t["port"] for t in data["tasks"] if t.get("status") == "running" and t.get("port")]
144
+
145
+
146
+ def check_port_conflict(port: int) -> dict | None:
147
+ """Check if a port is already in use. Returns conflicting task or None."""
148
+ tasks = get_tasks_by_port(port)
149
+ for t in tasks:
150
+ if t.get("status") == "running":
151
+ return t
152
+ return None
153
+
154
+
155
+ def format_task_list(tasks: list = None) -> str:
156
+ """Format task list for display to the user."""
157
+ if tasks is None:
158
+ tasks = get_active_tasks()
159
+
160
+ if not tasks:
161
+ return "No active background tasks."
162
+
163
+ lines = ["📋 Active Background Tasks:", ""]
164
+ for i, task in enumerate(tasks, 1):
165
+ status_icon = "🟢" if task.get("status") == "running" else "🔴"
166
+ type_icon = {
167
+ "tunnel": "🌐",
168
+ "server": "🖥️",
169
+ "webchat": "💬",
170
+ "static": "📁",
171
+ }.get(task.get("type", "server"), "⚙️")
172
+
173
+ url_str = f" → {task['url']}" if task.get("url") else ""
174
+ desc_str = f" — {task['description']}" if task.get("description") else ""
175
+
176
+ lines.append(
177
+ f"{i}. {status_icon} {type_icon} [{task['type']}] "
178
+ f"PID: {task['pid']}, Port: {task.get('port', 'N/A')}"
179
+ f"{url_str}{desc_str}"
180
+ )
181
+
182
+ lines.append("")
183
+ lines.append(f"Total: {len(tasks)} active task(s)")
184
+ return "\n".join(lines)
@@ -0,0 +1,80 @@
1
+ """Test MCP SSE connection and call send_telegram_message."""
2
+ import asyncio
3
+ import json
4
+ import anyio
5
+ from mcp.client.sse import sse_client
6
+ from mcp.types import JSONRPCMessage
7
+ from mcp.shared.message import SessionMessage
8
+
9
+
10
+ async def main():
11
+ print("Connecting to MCP SSE server at http://127.0.0.1:4097/sse ...")
12
+ async with sse_client(url="http://127.0.0.1:4097/sse") as (read, write):
13
+ print("Connected!")
14
+
15
+ # 1. Initialize
16
+ init = {
17
+ "jsonrpc": "2.0",
18
+ "id": 1,
19
+ "method": "initialize",
20
+ "params": {
21
+ "protocolVersion": "2024-11-05",
22
+ "capabilities": {},
23
+ "clientInfo": {"name": "bmo-bridge", "version": "1.0.0"},
24
+ },
25
+ }
26
+ msg = JSONRPCMessage.model_validate(init)
27
+ await write.send(SessionMessage(message=msg))
28
+
29
+ async for msg in read:
30
+ data = msg.message.model_dump()
31
+ print(f"Initialize response: {json.dumps(data, indent=2)[:300]}")
32
+ break
33
+
34
+ # 2. Send initialized notification
35
+ notif = {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}
36
+ msg = JSONRPCMessage.model_validate(notif)
37
+ await write.send(SessionMessage(message=msg))
38
+
39
+ # 3. List tools
40
+ list_tools = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
41
+ msg = JSONRPCMessage.model_validate(list_tools)
42
+ await write.send(SessionMessage(message=msg))
43
+
44
+ async for msg in read:
45
+ data = msg.message.model_dump()
46
+ if "result" in data:
47
+ tools = data["result"].get("tools", [])
48
+ print(f"\nFound {len(tools)} tools:")
49
+ for t in tools:
50
+ print(f" - {t['name']}: {t.get('description', '')[:80]}")
51
+ break
52
+
53
+ # 4. Call send_telegram_message
54
+ call = {
55
+ "jsonrpc": "2.0",
56
+ "id": 3,
57
+ "method": "tools/call",
58
+ "params": {
59
+ "name": "send_telegram_message",
60
+ "arguments": {
61
+ "chat_id": 732356803,
62
+ "message": "TEST from BMO MCP bridge ✅ Server tools connected!",
63
+ "parse_mode": "HTML",
64
+ },
65
+ },
66
+ }
67
+ print("\nCalling send_telegram_message...")
68
+ msg = JSONRPCMessage.model_validate(call)
69
+ await write.send(SessionMessage(message=msg))
70
+
71
+ async for msg in read:
72
+ data = msg.message.model_dump()
73
+ print(f"Result: {json.dumps(data, indent=2)[:500]}")
74
+ break
75
+
76
+ print("\nDone!")
77
+
78
+
79
+ if __name__ == "__main__":
80
+ asyncio.run(main())