@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
package/main.py ADDED
@@ -0,0 +1,125 @@
1
+ """
2
+ Main entry point for the OpenCode Telegram Bot.
3
+ """
4
+
5
+ import logging, os, sys
6
+
7
+ _BMO_ROOT = os.path.dirname(os.path.abspath(__file__))
8
+ if _BMO_ROOT not in sys.path:
9
+ sys.path.insert(0, _BMO_ROOT)
10
+
11
+ _PARENT_ROOT = os.path.dirname(_BMO_ROOT)
12
+ if _PARENT_ROOT not in sys.path:
13
+ sys.path.insert(0, _PARENT_ROOT)
14
+
15
+ try:
16
+ from config.settings import TELEGRAM_TOKEN, LOG_FORMAT, LOG_LEVEL
17
+ except ImportError as exc:
18
+ print(f"FATAL: Cannot import config.settings — {exc}", file=sys.stderr)
19
+ print(f" sys.path = {sys.path}", file=sys.stderr)
20
+ print(f" _BMO_ROOT = {_BMO_ROOT}", file=sys.stderr)
21
+ print(f" _PARENT_ROOT = {_PARENT_ROOT}", file=sys.stderr)
22
+ print(" Ensure you're running from the project root and config/settings.py exists.", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+ from telegram import Update
26
+ from telegram.ext import (
27
+ Application,
28
+ CallbackQueryHandler,
29
+ CommandHandler,
30
+ MessageHandler,
31
+ filters,
32
+ )
33
+
34
+ from handlers.messages import (
35
+ start_command,
36
+ menu_command,
37
+ choose_model_callback,
38
+ use_skill_callback,
39
+ session_select_callback,
40
+ handle_message,
41
+ handle_file,
42
+ mode_callback,
43
+ cancel_callback,
44
+ settings_callback,
45
+ agent_callback,
46
+ inline_action_callback,
47
+ )
48
+
49
+ logging.basicConfig(format=LOG_FORMAT, level=getattr(logging, LOG_LEVEL, logging.INFO))
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ def main():
54
+ if not TELEGRAM_TOKEN:
55
+ logger.error("TELEGRAM_TOKEN is not set. Create a .env file based on .env.example")
56
+ sys.exit(1)
57
+
58
+ # --- LOCK FILE MECHANISM: Prevent multiple instances ---
59
+ import os, psutil
60
+ lock_file = ".bot.lock"
61
+ pid = os.getpid()
62
+
63
+ if os.path.exists(lock_file):
64
+ try:
65
+ with open(lock_file, "r") as f:
66
+ old_pid = int(f.read().strip())
67
+ if psutil.pid_exists(old_pid):
68
+ logger.warning(f"⚠️ Conflict detected: Another BMO (PID {old_pid}) is already running!")
69
+ logger.warning("Terminating old instance to resolve conflict...")
70
+ try:
71
+ p = psutil.Process(old_pid)
72
+ p.terminate()
73
+ p.wait(timeout=3)
74
+ except Exception as e:
75
+ logger.error(f"Failed to kill old instance: {e}")
76
+ sys.exit(1)
77
+ except Exception:
78
+ pass # Stale or invalid lock
79
+
80
+ with open(lock_file, "w") as f:
81
+ f.write(str(pid))
82
+ # --------------------------------------------------------
83
+
84
+ logger.info("Starting OpenCode Telegram Bot (BMO)...")
85
+
86
+ app = Application.builder().token(TELEGRAM_TOKEN).build()
87
+
88
+ # ── Commands ──────────────────────────────────────────────────────────────
89
+ app.add_handler(CommandHandler("start", start_command))
90
+ app.add_handler(CommandHandler("menu", menu_command))
91
+
92
+ # ── Inline keyboard callbacks ─────────────────────────────────────────────
93
+ app.add_handler(CallbackQueryHandler(choose_model_callback, pattern=r"^(m|mp):"))
94
+ app.add_handler(CallbackQueryHandler(use_skill_callback, pattern=r"^skill:"))
95
+ app.add_handler(CallbackQueryHandler(cancel_callback, pattern=r"^cancel$"))
96
+ app.add_handler(CallbackQueryHandler(mode_callback, pattern=r"^mode_set:"))
97
+ app.add_handler(CallbackQueryHandler(session_select_callback, pattern=r"^session:"))
98
+ app.add_handler(CallbackQueryHandler(settings_callback, pattern=r"^(set_keys|admin_stats|back_settings|setup_p_.*|ttl_.*|perm_.*)$"))
99
+ app.add_handler(CallbackQueryHandler(agent_callback, pattern=r"^agent_"))
100
+ app.add_handler(CallbackQueryHandler(inline_action_callback, pattern=r"^(action:|back_to_menu|history_page:|session_load:)"))
101
+
102
+ # ── Plain text messages & Documents & Photos ───────────────────────────────────────
103
+ app.add_handler(MessageHandler(filters.Document.ALL | filters.PHOTO, handle_file))
104
+ app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
105
+
106
+ # ── Start MCP Server in Background ──
107
+ import threading
108
+ import uvicorn
109
+ from tools.mcp_server import mcp
110
+
111
+ def start_mcp():
112
+ uvicorn.run(mcp.sse_app, host="127.0.0.1", port=4097, log_level="error")
113
+
114
+ logger.info("Starting BMO MCP Server on port 4097...")
115
+ threading.Thread(target=start_mcp, daemon=True).start()
116
+
117
+ logger.info("BMO is online and polling...")
118
+ app.run_polling(allowed_updates=Update.ALL_TYPES)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ try:
123
+ main()
124
+ except KeyboardInterrupt:
125
+ print("Bot stopped.")
package/memory.md ADDED
@@ -0,0 +1,43 @@
1
+ # BMO Core Profile
2
+
3
+ ## User Profile
4
+
5
+ - Name: Aliwey
6
+ - Role: Developer / System Owner
7
+ - Preference: Direct and expert-level communication.
8
+
9
+ ## Learned Preferences
10
+
11
+ - Language: English only (never respond in Arabic).
12
+ - Communication: Concise, professional, and autonomous.
13
+ - Telegram Format: **Markdown** (integrated with CLI markdown output) — Telegram wire format uses MarkdownV2 parse mode, converted from standard markdown at send-time
14
+ - Project: BMO Telegram Bot (OpenCode Ecosystem).
15
+
16
+ ## Knowledge Index
17
+
18
+ This index summarizes technical achievements stored in `data/experience/`. BMO should read these files to reuse past solutions.
19
+
20
+ - **bmo_rebranding.md**: Full identity migration from NOVA to BMO (Core, Handlers, Web UI).
21
+ - **auto_summarization.md**: Implementation of idle-triggered session summaries with database persistence.
22
+ - **file_archival_system.md**: Session-linked file detection, copy, and indexing logic.
23
+ - **excel_inventory_automation.md**: Creation of multi-sheet Excel workbooks with cross-references for store management.
24
+ - **inline_menu_sessions.md**: Inline keyboard menu system with session history pagination and loading.
25
+ - **model_switching.md**: Model change with full session continuity — clears OpenCode session ID, creates fresh backend, injects SQLite history as context.
26
+ - **auto_title_summary.md**: Auto-summary now generates both summary + title after inactivity; new user guidance in system prompt to explain keyboard buttons.
27
+ - **webchat_public_default.md**: Webchat is always public — start server + cloudflared tunnel automatically. Never localhost-only unless user explicitly requests it. Full workflow: start_web_task → tunnel_webchat → wait 15s → check_task_status → send URL.
28
+ - **background_task_registry.md**: Full background task management system with JSON registry, MCP tools (start_web_task, list_background_tasks, check_task_status, stop_background_task, tunnel_webchat), auto-cleanup of dead processes, port conflict detection, and centralized logging. Prevents all bot freezing from servers/tunnels.
29
+ - **soft_new_session.md**: `/new` rewritten with Claude Code-style soft-reset — wipes model context via `delete_messages` but preserves `opencode_session_id` for warm system-prompt cache. Inherits provider/model/agent/mode from old session. Fixes 60s+ first-token latency on every `/new`.
30
+ - **cli_hang_timeout.md**: Fixed 20-min CLI hang — lowered `OPENCODE_TIMEOUT` 1200→120, wrapped `_do_send` POST in `asyncio.wait_for`, installed real OS-level `signal.SIGINT` handler in `cli.py` so Ctrl+C cancels the active streaming task even inside `rich.Live` + `prompt_toolkit.patch_stdout(raw=True)`.
31
+ - **cli_worker_kill.md**: Full worker-subprocess Ctrl+C fix — added `cancel_current()` to both `AsyncSubprocessWorker` and `MultiprocessWorker` + `WorkerManager` facade, SIGINT handler now kills the subprocess via `asyncio.run_coroutine_threadsafe(worker.cancel_current(), loop)` BEFORE cancelling the asyncio task (shielded futures would otherwise block propagation). `ensure_worker` checks `is_alive` so cancelled workers respawn on next request. Timeouts set to 900s (15 min) for reasoning-heavy tasks.
32
+ - **name_correction.md**: Fixed stale `Aliwi` → `Aliwey` in both profile files (`memory.md` + `data/memory.md`); reminder that the profile lives in two synced locations.
33
+ - **cli_exit_unclosed_transport.md**: Fixed `/exit` dumping 4 `ValueError: I/O operation on closed pipe` warnings. CLI disabled worker (`client._worker = None`) but `ensure_worker()` still spawned a stranded subprocess. Three-layer fix: skip worker start when client disables it + properly close pipe transports in `shutdown()` + post-loop cleanup in `cli.py`.
34
+ - **telegram_markdown_integration.md**: User preference flip — Telegram now uses Markdown (MarkdownV2 on the wire) integrated with CLI markdown output. Old HTML parser path must be replaced by a `to_markdown_v2()` converter + chunker. `_clean_telegram_html` is obsolete.
35
+ - **portable_distribution.md**: Made BMO distributable — each user runs their own instance. Configurable `OWNER_ID`, generic memory.md templates, path-agnostic build script, setup wizard with owner ID prompt, stripped all personal/dev artifacts, port 4800 default.
36
+ - **cli_spinner_inplace_update.md**: Fixed thinking-counter leaving a vertical trail of past values on screen. `rich.Console.print(..., end="")` does NOT honor `\r` — switched the tick update to `sys.stdout.write + flush` with manual ANSI dim codes, and clear the partial line on completion.
37
+ - **bfp_protocol.md**: BFP (BMO Friendship Protocol) — DID-based identity, A2A-compatible Agent Cards, WebSocket transport, relay discovery, and enterprise A2A bridge. BMO is the reference implementation.
38
+ - **bfp_integration_fix.md**: Fixed 3 critical bugs from parallel subagent isolation: A2A bridge sync/async mismatch calling `send_message()` with wrong args, `bfp.delegate` returning fake stubs. Extracted shared `core/bfp_tasks.py` module used by both transport and A2A bridge. 13 passing tests.
39
+ - **bfp_user_facing.md**: Discovery module (`bfp_discovery.py`), relay WS connector (`bfp_connector.py`), relay WS DID mapping + peer forwarding, CLI `/bfp status/find/delegate/talk` commands, `BFP_RELAY_URL` env var. Relay WS forwarding architecture: `send` action creates pending future, forwards to target's WS, awaits `relay_response`, returns result. All 20 BFP tests pass.
40
+ - **bfp_websocket_http_graceful.md**: Fixed `InvalidUpgrade: Keep-Alive` traceback when HTTP requests hit the BFP WebSocket port 8765. Added `_process_request` callback to `ws_serve()` that returns a 426 "Upgrade Required" response for non-WebSocket connections instead of letting the library crash.
41
+
42
+ ---
43
+ *BMO: Always update this index after adding a new file to the vault.*
File without changes
@@ -0,0 +1,143 @@
1
+ """
2
+ Data models for chat messages and sessions
3
+ """
4
+
5
+ from dataclasses import dataclass, asdict
6
+ from datetime import datetime
7
+ from typing import Optional, List
8
+ import json
9
+
10
+
11
+ @dataclass
12
+ class ChatMessage:
13
+ sender: str
14
+ content: str
15
+ timestamp: float
16
+ message_id: Optional[int] = None
17
+
18
+ def to_dict(self):
19
+ return asdict(self)
20
+
21
+ @classmethod
22
+ def from_dict(cls, data):
23
+ return cls(**data)
24
+
25
+
26
+ @dataclass
27
+ class ChatSession:
28
+ chat_id: int
29
+ user_id: int
30
+ username: Optional[str]
31
+ created_at: float
32
+ updated_at: float
33
+ messages: List[dict]
34
+ metadata: dict
35
+ title: str = ""
36
+ session_id: str = ""
37
+
38
+ def to_dict(self):
39
+ return {
40
+ "chat_id": self.chat_id,
41
+ "user_id": self.user_id,
42
+ "username": self.username,
43
+ "created_at": self.created_at,
44
+ "updated_at": self.updated_at,
45
+ "messages": self.messages,
46
+ "metadata": self.metadata,
47
+ "title": self.title,
48
+ "session_id": self.session_id,
49
+ }
50
+
51
+ @classmethod
52
+ def from_dict(cls, data):
53
+ for field in ("title", "session_id"):
54
+ if field not in data:
55
+ data[field] = ""
56
+ return cls(**data)
57
+
58
+ def add_message(self, sender: str, content: str, message_id: Optional[int] = None):
59
+ message = ChatMessage(
60
+ sender=sender,
61
+ content=content,
62
+ timestamp=datetime.now().timestamp(),
63
+ message_id=message_id,
64
+ )
65
+ self.messages.append(message.to_dict())
66
+ self.updated_at = datetime.now().timestamp()
67
+
68
+ def get_last_n_messages(self, n: int) -> List[ChatMessage]:
69
+ return [ChatMessage.from_dict(m) for m in self.messages[-n:]]
70
+
71
+ def get_context_text(self, max_messages: int = 10) -> str:
72
+ recent = self.get_last_n_messages(max_messages)
73
+ context = []
74
+ for msg in recent:
75
+ prefix = "You:" if msg.sender == "user" else "Assistant:"
76
+ context.append(f"{prefix} {msg.content}")
77
+ return "\n".join(context)
78
+
79
+ def set_title(self, title: str):
80
+ self.title = title
81
+ self.updated_at = datetime.now().timestamp()
82
+
83
+ def get_title(self) -> str:
84
+ return self.title
85
+
86
+ def set_summary(self, summary: str):
87
+ self.metadata["session_summary"] = summary
88
+ self.updated_at = datetime.now().timestamp()
89
+
90
+ def get_summary(self) -> Optional[str]:
91
+ return self.metadata.get("session_summary")
92
+
93
+ def set_description(self, description: str):
94
+ self.metadata["session_description"] = description
95
+ self.updated_at = datetime.now().timestamp()
96
+
97
+ def get_description(self) -> Optional[str]:
98
+ return self.metadata.get("session_description")
99
+
100
+ def get_message_counter(self) -> int:
101
+ return self.metadata.get("message_counter", 0)
102
+
103
+ def increment_message_counter(self) -> int:
104
+ count = self.metadata.get("message_counter", 0) + 1
105
+ self.metadata["message_counter"] = count
106
+ self.updated_at = datetime.now().timestamp()
107
+ return count
108
+
109
+ def reset_message_counter(self):
110
+ self.metadata["message_counter"] = 0
111
+ self.updated_at = datetime.now().timestamp()
112
+
113
+
114
+ @dataclass
115
+ class UserMemory:
116
+ user_id: int
117
+ chat_id: int
118
+ preferences: dict
119
+ knowledge: dict
120
+ created_at: float
121
+ updated_at: float
122
+
123
+ def to_dict(self):
124
+ return {
125
+ "user_id": self.user_id,
126
+ "chat_id": self.chat_id,
127
+ "preferences": self.preferences,
128
+ "knowledge": self.knowledge,
129
+ "created_at": self.created_at,
130
+ "updated_at": self.updated_at,
131
+ }
132
+
133
+ @classmethod
134
+ def from_dict(cls, data):
135
+ return cls(**data)
136
+
137
+ def update_preference(self, key: str, value):
138
+ self.preferences[key] = value
139
+ self.updated_at = datetime.now().timestamp()
140
+
141
+ def add_knowledge(self, key: str, value):
142
+ self.knowledge[key] = value
143
+ self.updated_at = datetime.now().timestamp()
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@aliwey/bmo",
3
+ "version": "2.0.0",
4
+ "description": "BMO — AI coding assistant with Telegram, CLI & Web sync. One command, all frontends.",
5
+ "keywords": ["ai", "coding-assistant", "telegram-bot", "cli", "opencode", "bfp"],
6
+ "homepage": "https://github.com/aliwey/bmo",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/aliwey/bmo.git"
10
+ },
11
+ "license": "MIT",
12
+ "author": "Aliwey",
13
+ "bin": {
14
+ "bmo": "bin/bmo.js"
15
+ },
16
+ "scripts": {
17
+ "postinstall": "node scripts/postinstall.js"
18
+ },
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "peerDependencies": {
23
+ "opencode-ai": ">=0.1.0"
24
+ },
25
+ "peerDependenciesMeta": {
26
+ "opencode-ai": {
27
+ "optional": true
28
+ }
29
+ },
30
+ "files": [
31
+ "bin/",
32
+ "scripts/",
33
+ "registry/",
34
+ "core/",
35
+ "handlers/",
36
+ "config/",
37
+ "models/",
38
+ "storage/",
39
+ "tools/",
40
+ "webchat/public/",
41
+ "webchat/server.js",
42
+ "webchat/package.json",
43
+ "webchat/package-lock.json",
44
+ "requirements.txt",
45
+ "setup.py",
46
+ "main.py",
47
+ "cli.py",
48
+ "memory.md"
49
+ ]
50
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * BFP Registry — Cloudflare Worker
3
+ * Deploy: wrangler deploy
4
+ * URL: https://bfp-registry.aliwey.workers.dev
5
+ *
6
+ * KV namespace: REGISTRY (binding name)
7
+ * Stores: DID → { endpoint, caps, ts }
8
+ * Auto-expiry: 1 hour (refreshed every 30min by bmo relay)
9
+ */
10
+
11
+ export default {
12
+ async fetch(req, env) {
13
+ const url = new URL(req.url);
14
+ const { pathname } = url;
15
+
16
+ // CORS headers for all responses
17
+ const corsHeaders = {
18
+ 'Access-Control-Allow-Origin': '*',
19
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
20
+ 'Access-Control-Allow-Headers': 'Content-Type',
21
+ };
22
+
23
+ if (req.method === 'OPTIONS') {
24
+ return new Response(null, { headers: corsHeaders });
25
+ }
26
+
27
+ const json = (data, status = 200) =>
28
+ Response.json(data, { status, headers: corsHeaders });
29
+
30
+ const err = (msg, status = 400) =>
31
+ json({ error: msg }, status);
32
+
33
+ // ── POST /register ────────────────────────────────────────────────────────
34
+ // Body: { did: string, endpoint: string, caps: string[] }
35
+ if (req.method === 'POST' && pathname === '/register') {
36
+ let body;
37
+ try { body = await req.json(); } catch { return err('Invalid JSON'); }
38
+
39
+ const { did, endpoint, caps } = body;
40
+ if (!did || !endpoint) return err('Missing did or endpoint');
41
+ if (!did.startsWith('did:bfp:')) return err('Invalid DID format');
42
+ if (!endpoint.startsWith('ws')) return err('Endpoint must be a WebSocket URL');
43
+
44
+ await env.REGISTRY.put(
45
+ did,
46
+ JSON.stringify({ endpoint, caps: caps || [], ts: Date.now() }),
47
+ { expirationTtl: 3600 } // auto-expire in 1 hour
48
+ );
49
+
50
+ return json({ ok: true, did, expires_in: 3600 });
51
+ }
52
+
53
+ // ── POST /unregister ──────────────────────────────────────────────────────
54
+ // Body: { did: string }
55
+ if (req.method === 'POST' && pathname === '/unregister') {
56
+ let body;
57
+ try { body = await req.json(); } catch { return err('Invalid JSON'); }
58
+
59
+ const { did } = body;
60
+ if (!did) return err('Missing did');
61
+
62
+ await env.REGISTRY.delete(did);
63
+ return json({ ok: true });
64
+ }
65
+
66
+ // ── GET /lookup?did=xxx ───────────────────────────────────────────────────
67
+ if (req.method === 'GET' && pathname === '/lookup') {
68
+ const did = url.searchParams.get('did');
69
+ if (!did) return err('Missing did parameter');
70
+
71
+ const val = await env.REGISTRY.get(did);
72
+ if (!val) return json({ error: 'Agent not found or offline' }, 404);
73
+
74
+ return json({ did, ...JSON.parse(val) });
75
+ }
76
+
77
+ // ── GET /list?capability=code ─────────────────────────────────────────────
78
+ // Returns all online agents, optionally filtered by capability
79
+ if (req.method === 'GET' && pathname === '/list') {
80
+ const cap = url.searchParams.get('capability');
81
+
82
+ const { keys } = await env.REGISTRY.list({ limit: 500 });
83
+ const agents = [];
84
+
85
+ await Promise.all(keys.map(async key => {
86
+ const val = await env.REGISTRY.get(key.name);
87
+ if (!val) return;
88
+ const data = JSON.parse(val);
89
+ if (!cap || (data.caps && data.caps.includes(cap))) {
90
+ agents.push({ did: key.name, ...data });
91
+ }
92
+ }));
93
+
94
+ // Sort by most recently registered
95
+ agents.sort((a, b) => (b.ts || 0) - (a.ts || 0));
96
+
97
+ return json(agents);
98
+ }
99
+
100
+ // ── GET /health ───────────────────────────────────────────────────────────
101
+ if (req.method === 'GET' && pathname === '/health') {
102
+ const { keys } = await env.REGISTRY.list({ limit: 1 });
103
+ return json({ status: 'ok', online: keys.length });
104
+ }
105
+
106
+ return json({ error: 'Not found' }, 404);
107
+ }
108
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "bfp-registry",
3
+ "main": "worker.js",
4
+ "compatibility_date": "2025-01-01",
5
+ "kv_namespaces": [
6
+ {
7
+ "binding": "REGISTRY",
8
+ "id": "REPLACE_WITH_YOUR_KV_NAMESPACE_ID"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,13 @@
1
+ python-telegram-bot>=20.0
2
+ python-dotenv>=1.0.0
3
+ httpx>=0.28.0
4
+ cryptography>=42.0.0
5
+ fastapi>=0.100.0
6
+ uvicorn>=0.22.0
7
+ mcp>=1.0.0
8
+ prompt-toolkit>=3.0
9
+ rich>=13.0
10
+ psutil
11
+ watchfiles
12
+ PyNaCl>=1.5.0
13
+ base58>=2.1.0
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BMO Setup Wizard — bmo init
4
+ * Generates ~/.bmo/.env interactively on first run.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const readline = require('readline');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const BMO_HOME = process.env.BMO_HOME || path.join(os.homedir(), '.bmo');
15
+ const ENV_PATH = path.join(BMO_HOME, '.env');
16
+
17
+ function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
18
+
19
+ function ask(rl, question, defaultValue) {
20
+ return new Promise(resolve => {
21
+ const hint = defaultValue ? ` [${defaultValue}]` : '';
22
+ rl.question(` ${question}${hint}: `, answer => {
23
+ resolve(answer.trim() || defaultValue || '');
24
+ });
25
+ });
26
+ }
27
+
28
+ (async () => {
29
+ console.log('\n╭───────────────────────────────────────────────────╮');
30
+ console.log('│ BMO Setup Wizard │');
31
+ console.log('╰───────────────────────────────────────────────────╯\n');
32
+
33
+ // Load existing .env values as defaults
34
+ let existing = {};
35
+ if (fs.existsSync(ENV_PATH)) {
36
+ const lines = fs.readFileSync(ENV_PATH, 'utf8').split('\n');
37
+ for (const line of lines) {
38
+ const m = line.match(/^([^#=]+)=(.*)$/);
39
+ if (m) existing[m[1].trim()] = m[2].trim();
40
+ }
41
+ console.log(` ℹ️ Existing config found at ${ENV_PATH} — press Enter to keep values.\n`);
42
+ }
43
+
44
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
45
+
46
+ // ── Telegram ────────────────────────────────────────────────────────────────
47
+ console.log('── Telegram ────────────────────────────────────────');
48
+ console.log(' Get your token from @BotFather on Telegram.');
49
+ const token = await ask(rl, 'Bot Token', existing.TELEGRAM_TOKEN || existing.TELEGRAM_BOT_TOKEN);
50
+
51
+ console.log('\n Your Telegram User ID (used for admin access):');
52
+ console.log(' → How to get it: message @userinfobot on Telegram');
53
+ console.log(' It will reply with your ID number.');
54
+ const userId = await ask(rl, 'Your Telegram User ID', existing.ALLOWED_USER_IDS);
55
+
56
+ // ── OpenCode ─────────────────────────────────────────────────────────────────
57
+ console.log('\n── OpenCode ─────────────────────────────────────────');
58
+ const ocPort = await ask(rl, 'OpenCode server port', existing.OPENCODE_PORT || '4800');
59
+
60
+ // ── Model ────────────────────────────────────────────────────────────────────
61
+ console.log('\n── AI Model ─────────────────────────────────────────');
62
+ console.log(' This is the default model used for conversations.');
63
+ const model = await ask(rl, 'Default model', existing.OPENCODE_MODEL || 'deepseek-v4-flash-free');
64
+ const provider = await ask(rl, 'Default provider', existing.OPENCODE_PROVIDER || 'opencode');
65
+
66
+ // ── BFP Registry ─────────────────────────────────────────────────────────────
67
+ console.log('\n── BFP Agent Discovery ──────────────────────────────');
68
+ console.log(' The registry lets other BMO agents discover yours.');
69
+ console.log(' Leave blank to use the default public registry.');
70
+ const bfpRegistry = await ask(
71
+ rl,
72
+ 'BFP Registry URL',
73
+ existing.BFP_REGISTRY_URL || 'https://bfp-registry.aliwey.workers.dev'
74
+ );
75
+
76
+ rl.close();
77
+
78
+ // ── Write .env ────────────────────────────────────────────────────────────────
79
+ mkdirp(BMO_HOME);
80
+ mkdirp(path.join(BMO_HOME, 'data'));
81
+ mkdirp(path.join(BMO_HOME, 'logs'));
82
+
83
+ const envContent = [
84
+ `# BMO Configuration — generated by bmo init`,
85
+ `# Location: ${ENV_PATH}`,
86
+ ``,
87
+ `# Telegram`,
88
+ `TELEGRAM_TOKEN=${token}`,
89
+ `ALLOWED_USER_IDS=${userId}`,
90
+ ``,
91
+ `# OpenCode`,
92
+ `OPENCODE_HOST=127.0.0.1`,
93
+ `OPENCODE_PORT=${ocPort}`,
94
+ ``,
95
+ `# AI Model`,
96
+ `OPENCODE_MODEL=${model}`,
97
+ `OPENCODE_PROVIDER=${provider}`,
98
+ ``,
99
+ `# BFP Agent Discovery`,
100
+ `BFP_REGISTRY_URL=${bfpRegistry}`,
101
+ ``,
102
+ `# Debug (set to True to enable verbose logging)`,
103
+ `DEBUG=False`,
104
+ ``,
105
+ ].join('\n');
106
+
107
+ fs.writeFileSync(ENV_PATH, envContent, 'utf8');
108
+
109
+ console.log('\n╭───────────────────────────────────────────────────╮');
110
+ console.log(`│ ✅ Config saved to ${ENV_PATH.padEnd(27)}│`);
111
+ console.log('│ │');
112
+ console.log('│ Run: bmo ← start BMO │');
113
+ console.log('│ bmo relay ← go online for BFP discovery │');
114
+ console.log('╰───────────────────────────────────────────────────╯\n');
115
+ })();