kyber-chat 1.0.0__py3-none-any.whl

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 (71) hide show
  1. kyber/__init__.py +6 -0
  2. kyber/__main__.py +8 -0
  3. kyber/agent/__init__.py +8 -0
  4. kyber/agent/context.py +224 -0
  5. kyber/agent/loop.py +687 -0
  6. kyber/agent/memory.py +109 -0
  7. kyber/agent/skills.py +244 -0
  8. kyber/agent/subagent.py +379 -0
  9. kyber/agent/tools/__init__.py +6 -0
  10. kyber/agent/tools/base.py +102 -0
  11. kyber/agent/tools/filesystem.py +191 -0
  12. kyber/agent/tools/message.py +86 -0
  13. kyber/agent/tools/registry.py +73 -0
  14. kyber/agent/tools/shell.py +141 -0
  15. kyber/agent/tools/spawn.py +65 -0
  16. kyber/agent/tools/task_status.py +53 -0
  17. kyber/agent/tools/web.py +163 -0
  18. kyber/bridge/package.json +26 -0
  19. kyber/bridge/src/index.ts +50 -0
  20. kyber/bridge/src/server.ts +104 -0
  21. kyber/bridge/src/types.d.ts +3 -0
  22. kyber/bridge/src/whatsapp.ts +185 -0
  23. kyber/bridge/tsconfig.json +16 -0
  24. kyber/bus/__init__.py +6 -0
  25. kyber/bus/events.py +37 -0
  26. kyber/bus/queue.py +81 -0
  27. kyber/channels/__init__.py +6 -0
  28. kyber/channels/base.py +121 -0
  29. kyber/channels/discord.py +304 -0
  30. kyber/channels/feishu.py +263 -0
  31. kyber/channels/manager.py +161 -0
  32. kyber/channels/telegram.py +302 -0
  33. kyber/channels/whatsapp.py +141 -0
  34. kyber/cli/__init__.py +1 -0
  35. kyber/cli/commands.py +736 -0
  36. kyber/config/__init__.py +6 -0
  37. kyber/config/loader.py +95 -0
  38. kyber/config/schema.py +205 -0
  39. kyber/cron/__init__.py +6 -0
  40. kyber/cron/service.py +346 -0
  41. kyber/cron/types.py +59 -0
  42. kyber/dashboard/__init__.py +5 -0
  43. kyber/dashboard/server.py +122 -0
  44. kyber/dashboard/static/app.js +458 -0
  45. kyber/dashboard/static/favicon.png +0 -0
  46. kyber/dashboard/static/index.html +107 -0
  47. kyber/dashboard/static/kyber_logo.png +0 -0
  48. kyber/dashboard/static/styles.css +608 -0
  49. kyber/heartbeat/__init__.py +5 -0
  50. kyber/heartbeat/service.py +130 -0
  51. kyber/providers/__init__.py +6 -0
  52. kyber/providers/base.py +69 -0
  53. kyber/providers/litellm_provider.py +227 -0
  54. kyber/providers/transcription.py +65 -0
  55. kyber/session/__init__.py +5 -0
  56. kyber/session/manager.py +202 -0
  57. kyber/skills/README.md +47 -0
  58. kyber/skills/github/SKILL.md +48 -0
  59. kyber/skills/skill-creator/SKILL.md +371 -0
  60. kyber/skills/summarize/SKILL.md +67 -0
  61. kyber/skills/tmux/SKILL.md +121 -0
  62. kyber/skills/tmux/scripts/find-sessions.sh +112 -0
  63. kyber/skills/tmux/scripts/wait-for-text.sh +83 -0
  64. kyber/skills/weather/SKILL.md +49 -0
  65. kyber/utils/__init__.py +5 -0
  66. kyber/utils/helpers.py +91 -0
  67. kyber_chat-1.0.0.dist-info/METADATA +35 -0
  68. kyber_chat-1.0.0.dist-info/RECORD +71 -0
  69. kyber_chat-1.0.0.dist-info/WHEEL +4 -0
  70. kyber_chat-1.0.0.dist-info/entry_points.txt +2 -0
  71. kyber_chat-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,86 @@
1
+ """Message tool for sending messages to users."""
2
+
3
+ from typing import Any, Callable, Awaitable
4
+
5
+ from kyber.agent.tools.base import Tool
6
+ from kyber.bus.events import OutboundMessage
7
+
8
+
9
+ class MessageTool(Tool):
10
+ """Tool to send messages to users on chat channels."""
11
+
12
+ def __init__(
13
+ self,
14
+ send_callback: Callable[[OutboundMessage], Awaitable[None]] | None = None,
15
+ default_channel: str = "",
16
+ default_chat_id: str = ""
17
+ ):
18
+ self._send_callback = send_callback
19
+ self._default_channel = default_channel
20
+ self._default_chat_id = default_chat_id
21
+
22
+ def set_context(self, channel: str, chat_id: str) -> None:
23
+ """Set the current message context."""
24
+ self._default_channel = channel
25
+ self._default_chat_id = chat_id
26
+
27
+ def set_send_callback(self, callback: Callable[[OutboundMessage], Awaitable[None]]) -> None:
28
+ """Set the callback for sending messages."""
29
+ self._send_callback = callback
30
+
31
+ @property
32
+ def name(self) -> str:
33
+ return "message"
34
+
35
+ @property
36
+ def description(self) -> str:
37
+ return "Send a message to the user. Use this when you want to communicate something."
38
+
39
+ @property
40
+ def parameters(self) -> dict[str, Any]:
41
+ return {
42
+ "type": "object",
43
+ "properties": {
44
+ "content": {
45
+ "type": "string",
46
+ "description": "The message content to send"
47
+ },
48
+ "channel": {
49
+ "type": "string",
50
+ "description": "Optional: target channel (telegram, discord, etc.)"
51
+ },
52
+ "chat_id": {
53
+ "type": "string",
54
+ "description": "Optional: target chat/user ID"
55
+ }
56
+ },
57
+ "required": ["content"]
58
+ }
59
+
60
+ async def execute(
61
+ self,
62
+ content: str,
63
+ channel: str | None = None,
64
+ chat_id: str | None = None,
65
+ **kwargs: Any
66
+ ) -> str:
67
+ channel = channel or self._default_channel
68
+ chat_id = chat_id or self._default_chat_id
69
+
70
+ if not channel or not chat_id:
71
+ return "Error: No target channel/chat specified"
72
+
73
+ if not self._send_callback:
74
+ return "Error: Message sending not configured"
75
+
76
+ msg = OutboundMessage(
77
+ channel=channel,
78
+ chat_id=chat_id,
79
+ content=content
80
+ )
81
+
82
+ try:
83
+ await self._send_callback(msg)
84
+ return f"Message sent to {channel}:{chat_id}"
85
+ except Exception as e:
86
+ return f"Error sending message: {str(e)}"
@@ -0,0 +1,73 @@
1
+ """Tool registry for dynamic tool management."""
2
+
3
+ from typing import Any
4
+
5
+ from kyber.agent.tools.base import Tool
6
+
7
+
8
+ class ToolRegistry:
9
+ """
10
+ Registry for agent tools.
11
+
12
+ Allows dynamic registration and execution of tools.
13
+ """
14
+
15
+ def __init__(self):
16
+ self._tools: dict[str, Tool] = {}
17
+
18
+ def register(self, tool: Tool) -> None:
19
+ """Register a tool."""
20
+ self._tools[tool.name] = tool
21
+
22
+ def unregister(self, name: str) -> None:
23
+ """Unregister a tool by name."""
24
+ self._tools.pop(name, None)
25
+
26
+ def get(self, name: str) -> Tool | None:
27
+ """Get a tool by name."""
28
+ return self._tools.get(name)
29
+
30
+ def has(self, name: str) -> bool:
31
+ """Check if a tool is registered."""
32
+ return name in self._tools
33
+
34
+ def get_definitions(self) -> list[dict[str, Any]]:
35
+ """Get all tool definitions in OpenAI format."""
36
+ return [tool.to_schema() for tool in self._tools.values()]
37
+
38
+ async def execute(self, name: str, params: dict[str, Any]) -> str:
39
+ """
40
+ Execute a tool by name with given parameters.
41
+
42
+ Args:
43
+ name: Tool name.
44
+ params: Tool parameters.
45
+
46
+ Returns:
47
+ Tool execution result as string.
48
+
49
+ Raises:
50
+ KeyError: If tool not found.
51
+ """
52
+ tool = self._tools.get(name)
53
+ if not tool:
54
+ return f"Error: Tool '{name}' not found"
55
+
56
+ try:
57
+ errors = tool.validate_params(params)
58
+ if errors:
59
+ return f"Error: Invalid parameters for tool '{name}': " + "; ".join(errors)
60
+ return await tool.execute(**params)
61
+ except Exception as e:
62
+ return f"Error executing {name}: {str(e)}"
63
+
64
+ @property
65
+ def tool_names(self) -> list[str]:
66
+ """Get list of registered tool names."""
67
+ return list(self._tools.keys())
68
+
69
+ def __len__(self) -> int:
70
+ return len(self._tools)
71
+
72
+ def __contains__(self, name: str) -> bool:
73
+ return name in self._tools
@@ -0,0 +1,141 @@
1
+ """Shell execution tool."""
2
+
3
+ import asyncio
4
+ import os
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from kyber.agent.tools.base import Tool
10
+
11
+
12
+ class ExecTool(Tool):
13
+ """Tool to execute shell commands."""
14
+
15
+ def __init__(
16
+ self,
17
+ timeout: int = 60,
18
+ working_dir: str | None = None,
19
+ deny_patterns: list[str] | None = None,
20
+ allow_patterns: list[str] | None = None,
21
+ restrict_to_workspace: bool = False,
22
+ ):
23
+ self.timeout = timeout
24
+ self.working_dir = working_dir
25
+ self.deny_patterns = deny_patterns or [
26
+ r"\brm\s+-[rf]{1,2}\b", # rm -r, rm -rf, rm -fr
27
+ r"\bdel\s+/[fq]\b", # del /f, del /q
28
+ r"\brmdir\s+/s\b", # rmdir /s
29
+ r"\b(format|mkfs|diskpart)\b", # disk operations
30
+ r"\bdd\s+if=", # dd
31
+ r">\s*/dev/sd", # write to disk
32
+ r"\b(shutdown|reboot|poweroff)\b", # system power
33
+ r":\(\)\s*\{.*\};\s*:", # fork bomb
34
+ ]
35
+ self.allow_patterns = allow_patterns or []
36
+ self.restrict_to_workspace = restrict_to_workspace
37
+
38
+ @property
39
+ def name(self) -> str:
40
+ return "exec"
41
+
42
+ @property
43
+ def description(self) -> str:
44
+ return "Execute a shell command and return its output. Use with caution."
45
+
46
+ @property
47
+ def parameters(self) -> dict[str, Any]:
48
+ return {
49
+ "type": "object",
50
+ "properties": {
51
+ "command": {
52
+ "type": "string",
53
+ "description": "The shell command to execute"
54
+ },
55
+ "working_dir": {
56
+ "type": "string",
57
+ "description": "Optional working directory for the command"
58
+ }
59
+ },
60
+ "required": ["command"]
61
+ }
62
+
63
+ async def execute(self, command: str, working_dir: str | None = None, **kwargs: Any) -> str:
64
+ cwd = working_dir or self.working_dir or os.getcwd()
65
+ guard_error = self._guard_command(command, cwd)
66
+ if guard_error:
67
+ return guard_error
68
+
69
+ try:
70
+ process = await asyncio.create_subprocess_shell(
71
+ command,
72
+ stdout=asyncio.subprocess.PIPE,
73
+ stderr=asyncio.subprocess.PIPE,
74
+ cwd=cwd,
75
+ )
76
+
77
+ try:
78
+ stdout, stderr = await asyncio.wait_for(
79
+ process.communicate(),
80
+ timeout=self.timeout
81
+ )
82
+ except asyncio.TimeoutError:
83
+ process.kill()
84
+ return f"Error: Command timed out after {self.timeout} seconds"
85
+
86
+ output_parts = []
87
+
88
+ if stdout:
89
+ output_parts.append(stdout.decode("utf-8", errors="replace"))
90
+
91
+ if stderr:
92
+ stderr_text = stderr.decode("utf-8", errors="replace")
93
+ if stderr_text.strip():
94
+ output_parts.append(f"STDERR:\n{stderr_text}")
95
+
96
+ if process.returncode != 0:
97
+ output_parts.append(f"\nExit code: {process.returncode}")
98
+
99
+ result = "\n".join(output_parts) if output_parts else "(no output)"
100
+
101
+ # Truncate very long output
102
+ max_len = 10000
103
+ if len(result) > max_len:
104
+ result = result[:max_len] + f"\n... (truncated, {len(result) - max_len} more chars)"
105
+
106
+ return result
107
+
108
+ except Exception as e:
109
+ return f"Error executing command: {str(e)}"
110
+
111
+ def _guard_command(self, command: str, cwd: str) -> str | None:
112
+ """Best-effort safety guard for potentially destructive commands."""
113
+ cmd = command.strip()
114
+ lower = cmd.lower()
115
+
116
+ for pattern in self.deny_patterns:
117
+ if re.search(pattern, lower):
118
+ return "Error: Command blocked by safety guard (dangerous pattern detected)"
119
+
120
+ if self.allow_patterns:
121
+ if not any(re.search(p, lower) for p in self.allow_patterns):
122
+ return "Error: Command blocked by safety guard (not in allowlist)"
123
+
124
+ if self.restrict_to_workspace:
125
+ if "..\\" in cmd or "../" in cmd:
126
+ return "Error: Command blocked by safety guard (path traversal detected)"
127
+
128
+ cwd_path = Path(cwd).resolve()
129
+
130
+ win_paths = re.findall(r"[A-Za-z]:\\[^\\\"']+", cmd)
131
+ posix_paths = re.findall(r"/[^\s\"']+", cmd)
132
+
133
+ for raw in win_paths + posix_paths:
134
+ try:
135
+ p = Path(raw).resolve()
136
+ except Exception:
137
+ continue
138
+ if cwd_path not in p.parents and p != cwd_path:
139
+ return "Error: Command blocked by safety guard (path outside working dir)"
140
+
141
+ return None
@@ -0,0 +1,65 @@
1
+ """Spawn tool for creating background subagents."""
2
+
3
+ from typing import Any, TYPE_CHECKING
4
+
5
+ from kyber.agent.tools.base import Tool
6
+
7
+ if TYPE_CHECKING:
8
+ from kyber.agent.subagent import SubagentManager
9
+
10
+
11
+ class SpawnTool(Tool):
12
+ """
13
+ Tool to spawn a subagent for background task execution.
14
+
15
+ The subagent runs asynchronously and announces its result back
16
+ to the main agent when complete.
17
+ """
18
+
19
+ def __init__(self, manager: "SubagentManager"):
20
+ self._manager = manager
21
+ self._origin_channel = "cli"
22
+ self._origin_chat_id = "direct"
23
+
24
+ def set_context(self, channel: str, chat_id: str) -> None:
25
+ """Set the origin context for subagent announcements."""
26
+ self._origin_channel = channel
27
+ self._origin_chat_id = chat_id
28
+
29
+ @property
30
+ def name(self) -> str:
31
+ return "spawn"
32
+
33
+ @property
34
+ def description(self) -> str:
35
+ return (
36
+ "Spawn a subagent to handle a task in the background. "
37
+ "Use this for complex or time-consuming tasks that can run independently. "
38
+ "The subagent will complete the task and report back when done."
39
+ )
40
+
41
+ @property
42
+ def parameters(self) -> dict[str, Any]:
43
+ return {
44
+ "type": "object",
45
+ "properties": {
46
+ "task": {
47
+ "type": "string",
48
+ "description": "The task for the subagent to complete",
49
+ },
50
+ "label": {
51
+ "type": "string",
52
+ "description": "Optional short label for the task (for display)",
53
+ },
54
+ },
55
+ "required": ["task"],
56
+ }
57
+
58
+ async def execute(self, task: str, label: str | None = None, **kwargs: Any) -> str:
59
+ """Spawn a subagent to execute the given task."""
60
+ return await self._manager.spawn(
61
+ task=task,
62
+ label=label,
63
+ origin_channel=self._origin_channel,
64
+ origin_chat_id=self._origin_chat_id,
65
+ )
@@ -0,0 +1,53 @@
1
+ """Task status tool for checking subagent progress."""
2
+
3
+ from typing import Any, TYPE_CHECKING
4
+
5
+ from kyber.agent.tools.base import Tool
6
+
7
+ if TYPE_CHECKING:
8
+ from kyber.agent.subagent import SubagentManager
9
+
10
+
11
+ class TaskStatusTool(Tool):
12
+ """
13
+ Instant-lookup tool for checking the status of background subagent tasks.
14
+
15
+ This is a lightweight tool that reads from an in-memory tracker — no LLM
16
+ calls or heavy work involved. The main agent should use this whenever the
17
+ user asks about the progress of a running task.
18
+ """
19
+
20
+ def __init__(self, manager: "SubagentManager"):
21
+ self._manager = manager
22
+
23
+ @property
24
+ def name(self) -> str:
25
+ return "task_status"
26
+
27
+ @property
28
+ def description(self) -> str:
29
+ return (
30
+ "Check the status and progress of background subagent tasks. "
31
+ "Returns live progress including current step, elapsed time, and "
32
+ "recent actions. Use this when the user asks about the status of "
33
+ "a running task. Call with no arguments to see all tasks, or pass "
34
+ "a task_id to check a specific one."
35
+ )
36
+
37
+ @property
38
+ def parameters(self) -> dict[str, Any]:
39
+ return {
40
+ "type": "object",
41
+ "properties": {
42
+ "task_id": {
43
+ "type": "string",
44
+ "description": "Optional: specific task ID to check. Omit to see all tasks.",
45
+ },
46
+ },
47
+ "required": [],
48
+ }
49
+
50
+ async def execute(self, task_id: str | None = None, **kwargs: Any) -> str:
51
+ if task_id:
52
+ return self._manager.get_task_status(task_id)
53
+ return self._manager.get_all_status()
@@ -0,0 +1,163 @@
1
+ """Web tools: web_search and web_fetch."""
2
+
3
+ import html
4
+ import json
5
+ import os
6
+ import re
7
+ from typing import Any
8
+ from urllib.parse import urlparse
9
+
10
+ import httpx
11
+
12
+ from kyber.agent.tools.base import Tool
13
+
14
+ # Shared constants
15
+ USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36"
16
+ MAX_REDIRECTS = 5 # Limit redirects to prevent DoS attacks
17
+
18
+
19
+ def _strip_tags(text: str) -> str:
20
+ """Remove HTML tags and decode entities."""
21
+ text = re.sub(r'<script[\s\S]*?</script>', '', text, flags=re.I)
22
+ text = re.sub(r'<style[\s\S]*?</style>', '', text, flags=re.I)
23
+ text = re.sub(r'<[^>]+>', '', text)
24
+ return html.unescape(text).strip()
25
+
26
+
27
+ def _normalize(text: str) -> str:
28
+ """Normalize whitespace."""
29
+ text = re.sub(r'[ \t]+', ' ', text)
30
+ return re.sub(r'\n{3,}', '\n\n', text).strip()
31
+
32
+
33
+ def _validate_url(url: str) -> tuple[bool, str]:
34
+ """Validate URL: must be http(s) with valid domain."""
35
+ try:
36
+ p = urlparse(url)
37
+ if p.scheme not in ('http', 'https'):
38
+ return False, f"Only http/https allowed, got '{p.scheme or 'none'}'"
39
+ if not p.netloc:
40
+ return False, "Missing domain"
41
+ return True, ""
42
+ except Exception as e:
43
+ return False, str(e)
44
+
45
+
46
+ class WebSearchTool(Tool):
47
+ """Search the web using Brave Search API."""
48
+
49
+ name = "web_search"
50
+ description = "Search the web. Returns titles, URLs, and snippets."
51
+ parameters = {
52
+ "type": "object",
53
+ "properties": {
54
+ "query": {"type": "string", "description": "Search query"},
55
+ "count": {"type": "integer", "description": "Results (1-10)", "minimum": 1, "maximum": 10}
56
+ },
57
+ "required": ["query"]
58
+ }
59
+
60
+ def __init__(self, api_key: str | None = None, max_results: int = 5):
61
+ self.api_key = api_key or os.environ.get("BRAVE_API_KEY", "")
62
+ self.max_results = max_results
63
+
64
+ async def execute(self, query: str, count: int | None = None, **kwargs: Any) -> str:
65
+ if not self.api_key:
66
+ return "Error: BRAVE_API_KEY not configured"
67
+
68
+ try:
69
+ n = min(max(count or self.max_results, 1), 10)
70
+ async with httpx.AsyncClient() as client:
71
+ r = await client.get(
72
+ "https://api.search.brave.com/res/v1/web/search",
73
+ params={"q": query, "count": n},
74
+ headers={"Accept": "application/json", "X-Subscription-Token": self.api_key},
75
+ timeout=10.0
76
+ )
77
+ r.raise_for_status()
78
+
79
+ results = r.json().get("web", {}).get("results", [])
80
+ if not results:
81
+ return f"No results for: {query}"
82
+
83
+ lines = [f"Results for: {query}\n"]
84
+ for i, item in enumerate(results[:n], 1):
85
+ lines.append(f"{i}. {item.get('title', '')}\n {item.get('url', '')}")
86
+ if desc := item.get("description"):
87
+ lines.append(f" {desc}")
88
+ return "\n".join(lines)
89
+ except Exception as e:
90
+ return f"Error: {e}"
91
+
92
+
93
+ class WebFetchTool(Tool):
94
+ """Fetch and extract content from a URL using Readability."""
95
+
96
+ name = "web_fetch"
97
+ description = "Fetch URL and extract readable content (HTML → markdown/text)."
98
+ parameters = {
99
+ "type": "object",
100
+ "properties": {
101
+ "url": {"type": "string", "description": "URL to fetch"},
102
+ "extractMode": {"type": "string", "enum": ["markdown", "text"], "default": "markdown"},
103
+ "maxChars": {"type": "integer", "minimum": 100}
104
+ },
105
+ "required": ["url"]
106
+ }
107
+
108
+ def __init__(self, max_chars: int = 50000):
109
+ self.max_chars = max_chars
110
+
111
+ async def execute(self, url: str, extractMode: str = "markdown", maxChars: int | None = None, **kwargs: Any) -> str:
112
+ from readability import Document
113
+
114
+ max_chars = maxChars or self.max_chars
115
+
116
+ # Validate URL before fetching
117
+ is_valid, error_msg = _validate_url(url)
118
+ if not is_valid:
119
+ return json.dumps({"error": f"URL validation failed: {error_msg}", "url": url})
120
+
121
+ try:
122
+ async with httpx.AsyncClient(
123
+ follow_redirects=True,
124
+ max_redirects=MAX_REDIRECTS,
125
+ timeout=30.0
126
+ ) as client:
127
+ r = await client.get(url, headers={"User-Agent": USER_AGENT})
128
+ r.raise_for_status()
129
+
130
+ ctype = r.headers.get("content-type", "")
131
+
132
+ # JSON
133
+ if "application/json" in ctype:
134
+ text, extractor = json.dumps(r.json(), indent=2), "json"
135
+ # HTML
136
+ elif "text/html" in ctype or r.text[:256].lower().startswith(("<!doctype", "<html")):
137
+ doc = Document(r.text)
138
+ content = self._to_markdown(doc.summary()) if extractMode == "markdown" else _strip_tags(doc.summary())
139
+ text = f"# {doc.title()}\n\n{content}" if doc.title() else content
140
+ extractor = "readability"
141
+ else:
142
+ text, extractor = r.text, "raw"
143
+
144
+ truncated = len(text) > max_chars
145
+ if truncated:
146
+ text = text[:max_chars]
147
+
148
+ return json.dumps({"url": url, "finalUrl": str(r.url), "status": r.status_code,
149
+ "extractor": extractor, "truncated": truncated, "length": len(text), "text": text})
150
+ except Exception as e:
151
+ return json.dumps({"error": str(e), "url": url})
152
+
153
+ def _to_markdown(self, html: str) -> str:
154
+ """Convert HTML to markdown."""
155
+ # Convert links, headings, lists before stripping tags
156
+ text = re.sub(r'<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>([\s\S]*?)</a>',
157
+ lambda m: f'[{_strip_tags(m[2])}]({m[1]})', html, flags=re.I)
158
+ text = re.sub(r'<h([1-6])[^>]*>([\s\S]*?)</h\1>',
159
+ lambda m: f'\n{"#" * int(m[1])} {_strip_tags(m[2])}\n', text, flags=re.I)
160
+ text = re.sub(r'<li[^>]*>([\s\S]*?)</li>', lambda m: f'\n- {_strip_tags(m[1])}', text, flags=re.I)
161
+ text = re.sub(r'</(p|div|section|article)>', '\n\n', text, flags=re.I)
162
+ text = re.sub(r'<(br|hr)\s*/?>', '\n', text, flags=re.I)
163
+ return _normalize(_strip_tags(text))
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "kyber-whatsapp-bridge",
3
+ "version": "0.1.0",
4
+ "description": "WhatsApp bridge for kyber using Baileys",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "dev": "tsc && node dist/index.js"
11
+ },
12
+ "dependencies": {
13
+ "@whiskeysockets/baileys": "7.0.0-rc.9",
14
+ "ws": "^8.17.0",
15
+ "qrcode-terminal": "^0.12.0",
16
+ "pino": "^9.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.14.0",
20
+ "@types/ws": "^8.5.10",
21
+ "typescript": "^5.4.0"
22
+ },
23
+ "engines": {
24
+ "node": ">=20.0.0"
25
+ }
26
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * kyber WhatsApp Bridge
4
+ *
5
+ * This bridge connects WhatsApp Web to kyber's Python backend
6
+ * via WebSocket. It handles authentication, message forwarding,
7
+ * and reconnection logic.
8
+ *
9
+ * Usage:
10
+ * npm run build && npm start
11
+ *
12
+ * Or with custom settings:
13
+ * BRIDGE_PORT=3001 AUTH_DIR=~/.kyber/whatsapp npm start
14
+ */
15
+
16
+ // Polyfill crypto for Baileys in ESM
17
+ import { webcrypto } from 'crypto';
18
+ if (!globalThis.crypto) {
19
+ (globalThis as any).crypto = webcrypto;
20
+ }
21
+
22
+ import { BridgeServer } from './server.js';
23
+ import { homedir } from 'os';
24
+ import { join } from 'path';
25
+
26
+ const PORT = parseInt(process.env.BRIDGE_PORT || '3001', 10);
27
+ const AUTH_DIR = process.env.AUTH_DIR || join(homedir(), '.kyber', 'whatsapp-auth');
28
+
29
+ console.log('💎 kyber WhatsApp Bridge');
30
+ console.log('========================\n');
31
+
32
+ const server = new BridgeServer(PORT, AUTH_DIR);
33
+
34
+ // Handle graceful shutdown
35
+ process.on('SIGINT', async () => {
36
+ console.log('\n\nShutting down...');
37
+ await server.stop();
38
+ process.exit(0);
39
+ });
40
+
41
+ process.on('SIGTERM', async () => {
42
+ await server.stop();
43
+ process.exit(0);
44
+ });
45
+
46
+ // Start the server
47
+ server.start().catch((error) => {
48
+ console.error('Failed to start bridge:', error);
49
+ process.exit(1);
50
+ });