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,104 @@
1
+ /**
2
+ * WebSocket server for Python-Node.js bridge communication.
3
+ */
4
+
5
+ import { WebSocketServer, WebSocket } from 'ws';
6
+ import { WhatsAppClient, InboundMessage } from './whatsapp.js';
7
+
8
+ interface SendCommand {
9
+ type: 'send';
10
+ to: string;
11
+ text: string;
12
+ }
13
+
14
+ interface BridgeMessage {
15
+ type: 'message' | 'status' | 'qr' | 'error';
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ export class BridgeServer {
20
+ private wss: WebSocketServer | null = null;
21
+ private wa: WhatsAppClient | null = null;
22
+ private clients: Set<WebSocket> = new Set();
23
+
24
+ constructor(private port: number, private authDir: string) {}
25
+
26
+ async start(): Promise<void> {
27
+ // Create WebSocket server
28
+ this.wss = new WebSocketServer({ port: this.port });
29
+ console.log(`🌉 Bridge server listening on ws://localhost:${this.port}`);
30
+
31
+ // Initialize WhatsApp client
32
+ this.wa = new WhatsAppClient({
33
+ authDir: this.authDir,
34
+ onMessage: (msg) => this.broadcast({ type: 'message', ...msg }),
35
+ onQR: (qr) => this.broadcast({ type: 'qr', qr }),
36
+ onStatus: (status) => this.broadcast({ type: 'status', status }),
37
+ });
38
+
39
+ // Handle WebSocket connections
40
+ this.wss.on('connection', (ws) => {
41
+ console.log('🔗 Python client connected');
42
+ this.clients.add(ws);
43
+
44
+ ws.on('message', async (data) => {
45
+ try {
46
+ const cmd = JSON.parse(data.toString()) as SendCommand;
47
+ await this.handleCommand(cmd);
48
+ ws.send(JSON.stringify({ type: 'sent', to: cmd.to }));
49
+ } catch (error) {
50
+ console.error('Error handling command:', error);
51
+ ws.send(JSON.stringify({ type: 'error', error: String(error) }));
52
+ }
53
+ });
54
+
55
+ ws.on('close', () => {
56
+ console.log('🔌 Python client disconnected');
57
+ this.clients.delete(ws);
58
+ });
59
+
60
+ ws.on('error', (error) => {
61
+ console.error('WebSocket error:', error);
62
+ this.clients.delete(ws);
63
+ });
64
+ });
65
+
66
+ // Connect to WhatsApp
67
+ await this.wa.connect();
68
+ }
69
+
70
+ private async handleCommand(cmd: SendCommand): Promise<void> {
71
+ if (cmd.type === 'send' && this.wa) {
72
+ await this.wa.sendMessage(cmd.to, cmd.text);
73
+ }
74
+ }
75
+
76
+ private broadcast(msg: BridgeMessage): void {
77
+ const data = JSON.stringify(msg);
78
+ for (const client of this.clients) {
79
+ if (client.readyState === WebSocket.OPEN) {
80
+ client.send(data);
81
+ }
82
+ }
83
+ }
84
+
85
+ async stop(): Promise<void> {
86
+ // Close all client connections
87
+ for (const client of this.clients) {
88
+ client.close();
89
+ }
90
+ this.clients.clear();
91
+
92
+ // Close WebSocket server
93
+ if (this.wss) {
94
+ this.wss.close();
95
+ this.wss = null;
96
+ }
97
+
98
+ // Disconnect WhatsApp
99
+ if (this.wa) {
100
+ await this.wa.disconnect();
101
+ this.wa = null;
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,3 @@
1
+ declare module 'qrcode-terminal' {
2
+ export function generate(text: string, options?: { small?: boolean }): void;
3
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * WhatsApp client wrapper using Baileys.
3
+ * Based on OpenClaw's working implementation.
4
+ */
5
+
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+ import makeWASocket, {
8
+ DisconnectReason,
9
+ useMultiFileAuthState,
10
+ fetchLatestBaileysVersion,
11
+ makeCacheableSignalKeyStore,
12
+ } from '@whiskeysockets/baileys';
13
+
14
+ import { Boom } from '@hapi/boom';
15
+ import qrcode from 'qrcode-terminal';
16
+ import pino from 'pino';
17
+
18
+ const VERSION = '0.1.0';
19
+
20
+ export interface InboundMessage {
21
+ id: string;
22
+ sender: string;
23
+ content: string;
24
+ timestamp: number;
25
+ isGroup: boolean;
26
+ }
27
+
28
+ export interface WhatsAppClientOptions {
29
+ authDir: string;
30
+ onMessage: (msg: InboundMessage) => void;
31
+ onQR: (qr: string) => void;
32
+ onStatus: (status: string) => void;
33
+ }
34
+
35
+ export class WhatsAppClient {
36
+ private sock: any = null;
37
+ private options: WhatsAppClientOptions;
38
+ private reconnecting = false;
39
+
40
+ constructor(options: WhatsAppClientOptions) {
41
+ this.options = options;
42
+ }
43
+
44
+ async connect(): Promise<void> {
45
+ const logger = pino({ level: 'silent' });
46
+ const { state, saveCreds } = await useMultiFileAuthState(this.options.authDir);
47
+ const { version } = await fetchLatestBaileysVersion();
48
+
49
+ console.log(`Using Baileys version: ${version.join('.')}`);
50
+
51
+ // Create socket following OpenClaw's pattern
52
+ this.sock = makeWASocket({
53
+ auth: {
54
+ creds: state.creds,
55
+ keys: makeCacheableSignalKeyStore(state.keys, logger),
56
+ },
57
+ version,
58
+ logger,
59
+ printQRInTerminal: false,
60
+ browser: ['kyber', 'cli', VERSION],
61
+ syncFullHistory: false,
62
+ markOnlineOnConnect: false,
63
+ });
64
+
65
+ // Handle WebSocket errors
66
+ if (this.sock.ws && typeof this.sock.ws.on === 'function') {
67
+ this.sock.ws.on('error', (err: Error) => {
68
+ console.error('WebSocket error:', err.message);
69
+ });
70
+ }
71
+
72
+ // Handle connection updates
73
+ this.sock.ev.on('connection.update', async (update: any) => {
74
+ const { connection, lastDisconnect, qr } = update;
75
+
76
+ if (qr) {
77
+ // Display QR code in terminal
78
+ console.log('\n📱 Scan this QR code with WhatsApp (Linked Devices):\n');
79
+ qrcode.generate(qr, { small: true });
80
+ this.options.onQR(qr);
81
+ }
82
+
83
+ if (connection === 'close') {
84
+ const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
85
+ const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
86
+
87
+ console.log(`Connection closed. Status: ${statusCode}, Will reconnect: ${shouldReconnect}`);
88
+ this.options.onStatus('disconnected');
89
+
90
+ if (shouldReconnect && !this.reconnecting) {
91
+ this.reconnecting = true;
92
+ console.log('Reconnecting in 5 seconds...');
93
+ setTimeout(() => {
94
+ this.reconnecting = false;
95
+ this.connect();
96
+ }, 5000);
97
+ }
98
+ } else if (connection === 'open') {
99
+ console.log('✅ Connected to WhatsApp');
100
+ this.options.onStatus('connected');
101
+ }
102
+ });
103
+
104
+ // Save credentials on update
105
+ this.sock.ev.on('creds.update', saveCreds);
106
+
107
+ // Handle incoming messages
108
+ this.sock.ev.on('messages.upsert', async ({ messages, type }: { messages: any[]; type: string }) => {
109
+ if (type !== 'notify') return;
110
+
111
+ for (const msg of messages) {
112
+ // Skip own messages
113
+ if (msg.key.fromMe) continue;
114
+
115
+ // Skip status updates
116
+ if (msg.key.remoteJid === 'status@broadcast') continue;
117
+
118
+ const content = this.extractMessageContent(msg);
119
+ if (!content) continue;
120
+
121
+ const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false;
122
+
123
+ this.options.onMessage({
124
+ id: msg.key.id || '',
125
+ sender: msg.key.remoteJid || '',
126
+ content,
127
+ timestamp: msg.messageTimestamp as number,
128
+ isGroup,
129
+ });
130
+ }
131
+ });
132
+ }
133
+
134
+ private extractMessageContent(msg: any): string | null {
135
+ const message = msg.message;
136
+ if (!message) return null;
137
+
138
+ // Text message
139
+ if (message.conversation) {
140
+ return message.conversation;
141
+ }
142
+
143
+ // Extended text (reply, link preview)
144
+ if (message.extendedTextMessage?.text) {
145
+ return message.extendedTextMessage.text;
146
+ }
147
+
148
+ // Image with caption
149
+ if (message.imageMessage?.caption) {
150
+ return `[Image] ${message.imageMessage.caption}`;
151
+ }
152
+
153
+ // Video with caption
154
+ if (message.videoMessage?.caption) {
155
+ return `[Video] ${message.videoMessage.caption}`;
156
+ }
157
+
158
+ // Document with caption
159
+ if (message.documentMessage?.caption) {
160
+ return `[Document] ${message.documentMessage.caption}`;
161
+ }
162
+
163
+ // Voice/Audio message
164
+ if (message.audioMessage) {
165
+ return `[Voice Message]`;
166
+ }
167
+
168
+ return null;
169
+ }
170
+
171
+ async sendMessage(to: string, text: string): Promise<void> {
172
+ if (!this.sock) {
173
+ throw new Error('Not connected');
174
+ }
175
+
176
+ await this.sock.sendMessage(to, { text });
177
+ }
178
+
179
+ async disconnect(): Promise<void> {
180
+ if (this.sock) {
181
+ this.sock.end(undefined);
182
+ this.sock = null;
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "declaration": true,
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
kyber/bus/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Message bus module for decoupled channel-agent communication."""
2
+
3
+ from kyber.bus.events import InboundMessage, OutboundMessage
4
+ from kyber.bus.queue import MessageBus
5
+
6
+ __all__ = ["MessageBus", "InboundMessage", "OutboundMessage"]
kyber/bus/events.py ADDED
@@ -0,0 +1,37 @@
1
+ """Event types for the message bus."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+
8
+ @dataclass
9
+ class InboundMessage:
10
+ """Message received from a chat channel."""
11
+
12
+ channel: str # telegram, discord, slack, whatsapp
13
+ sender_id: str # User identifier
14
+ chat_id: str # Chat/channel identifier
15
+ content: str # Message text
16
+ timestamp: datetime = field(default_factory=datetime.now)
17
+ media: list[str] = field(default_factory=list) # Media URLs
18
+ metadata: dict[str, Any] = field(default_factory=dict) # Channel-specific data
19
+
20
+ @property
21
+ def session_key(self) -> str:
22
+ """Unique key for session identification."""
23
+ return f"{self.channel}:{self.chat_id}"
24
+
25
+
26
+ @dataclass
27
+ class OutboundMessage:
28
+ """Message to send to a chat channel."""
29
+
30
+ channel: str
31
+ chat_id: str
32
+ content: str
33
+ reply_to: str | None = None
34
+ media: list[str] = field(default_factory=list)
35
+ metadata: dict[str, Any] = field(default_factory=dict)
36
+
37
+
kyber/bus/queue.py ADDED
@@ -0,0 +1,81 @@
1
+ """Async message queue for decoupled channel-agent communication."""
2
+
3
+ import asyncio
4
+ from typing import Callable, Awaitable
5
+
6
+ from loguru import logger
7
+
8
+ from kyber.bus.events import InboundMessage, OutboundMessage
9
+
10
+
11
+ class MessageBus:
12
+ """
13
+ Async message bus that decouples chat channels from the agent core.
14
+
15
+ Channels push messages to the inbound queue, and the agent processes
16
+ them and pushes responses to the outbound queue.
17
+ """
18
+
19
+ def __init__(self):
20
+ self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue()
21
+ self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue()
22
+ self._outbound_subscribers: dict[str, list[Callable[[OutboundMessage], Awaitable[None]]]] = {}
23
+ self._running = False
24
+
25
+ async def publish_inbound(self, msg: InboundMessage) -> None:
26
+ """Publish a message from a channel to the agent."""
27
+ await self.inbound.put(msg)
28
+
29
+ async def consume_inbound(self) -> InboundMessage:
30
+ """Consume the next inbound message (blocks until available)."""
31
+ return await self.inbound.get()
32
+
33
+ async def publish_outbound(self, msg: OutboundMessage) -> None:
34
+ """Publish a response from the agent to channels."""
35
+ await self.outbound.put(msg)
36
+
37
+ async def consume_outbound(self) -> OutboundMessage:
38
+ """Consume the next outbound message (blocks until available)."""
39
+ return await self.outbound.get()
40
+
41
+ def subscribe_outbound(
42
+ self,
43
+ channel: str,
44
+ callback: Callable[[OutboundMessage], Awaitable[None]]
45
+ ) -> None:
46
+ """Subscribe to outbound messages for a specific channel."""
47
+ if channel not in self._outbound_subscribers:
48
+ self._outbound_subscribers[channel] = []
49
+ self._outbound_subscribers[channel].append(callback)
50
+
51
+ async def dispatch_outbound(self) -> None:
52
+ """
53
+ Dispatch outbound messages to subscribed channels.
54
+ Run this as a background task.
55
+ """
56
+ self._running = True
57
+ while self._running:
58
+ try:
59
+ msg = await asyncio.wait_for(self.outbound.get(), timeout=1.0)
60
+ subscribers = self._outbound_subscribers.get(msg.channel, [])
61
+ for callback in subscribers:
62
+ try:
63
+ await callback(msg)
64
+ except Exception as e:
65
+ logger.error(f"Error dispatching to {msg.channel}: {e}")
66
+ except asyncio.TimeoutError:
67
+ continue
68
+
69
+ def stop(self) -> None:
70
+ """Stop the dispatcher loop."""
71
+ self._running = False
72
+
73
+ @property
74
+ def inbound_size(self) -> int:
75
+ """Number of pending inbound messages."""
76
+ return self.inbound.qsize()
77
+
78
+ @property
79
+ def outbound_size(self) -> int:
80
+ """Number of pending outbound messages."""
81
+ return self.outbound.qsize()
@@ -0,0 +1,6 @@
1
+ """Chat channels module with plugin architecture."""
2
+
3
+ from kyber.channels.base import BaseChannel
4
+ from kyber.channels.manager import ChannelManager
5
+
6
+ __all__ = ["BaseChannel", "ChannelManager"]
kyber/channels/base.py ADDED
@@ -0,0 +1,121 @@
1
+ """Base channel interface for chat platforms."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ from kyber.bus.events import InboundMessage, OutboundMessage
7
+ from kyber.bus.queue import MessageBus
8
+
9
+
10
+ class BaseChannel(ABC):
11
+ """
12
+ Abstract base class for chat channel implementations.
13
+
14
+ Each channel (Telegram, Discord, etc.) should implement this interface
15
+ to integrate with the kyber message bus.
16
+ """
17
+
18
+ name: str = "base"
19
+
20
+ def __init__(self, config: Any, bus: MessageBus):
21
+ """
22
+ Initialize the channel.
23
+
24
+ Args:
25
+ config: Channel-specific configuration.
26
+ bus: The message bus for communication.
27
+ """
28
+ self.config = config
29
+ self.bus = bus
30
+ self._running = False
31
+
32
+ @abstractmethod
33
+ async def start(self) -> None:
34
+ """
35
+ Start the channel and begin listening for messages.
36
+
37
+ This should be a long-running async task that:
38
+ 1. Connects to the chat platform
39
+ 2. Listens for incoming messages
40
+ 3. Forwards messages to the bus via _handle_message()
41
+ """
42
+ pass
43
+
44
+ @abstractmethod
45
+ async def stop(self) -> None:
46
+ """Stop the channel and clean up resources."""
47
+ pass
48
+
49
+ @abstractmethod
50
+ async def send(self, msg: OutboundMessage) -> None:
51
+ """
52
+ Send a message through this channel.
53
+
54
+ Args:
55
+ msg: The message to send.
56
+ """
57
+ pass
58
+
59
+ def is_allowed(self, sender_id: str) -> bool:
60
+ """
61
+ Check if a sender is allowed to use this bot.
62
+
63
+ Args:
64
+ sender_id: The sender's identifier.
65
+
66
+ Returns:
67
+ True if allowed, False otherwise.
68
+ """
69
+ allow_list = getattr(self.config, "allow_from", [])
70
+
71
+ # If no allow list, allow everyone
72
+ if not allow_list:
73
+ return True
74
+
75
+ sender_str = str(sender_id)
76
+ if sender_str in allow_list:
77
+ return True
78
+ if "|" in sender_str:
79
+ for part in sender_str.split("|"):
80
+ if part and part in allow_list:
81
+ return True
82
+ return False
83
+
84
+ async def _handle_message(
85
+ self,
86
+ sender_id: str,
87
+ chat_id: str,
88
+ content: str,
89
+ media: list[str] | None = None,
90
+ metadata: dict[str, Any] | None = None
91
+ ) -> None:
92
+ """
93
+ Handle an incoming message from the chat platform.
94
+
95
+ This method checks permissions and forwards to the bus.
96
+
97
+ Args:
98
+ sender_id: The sender's identifier.
99
+ chat_id: The chat/channel identifier.
100
+ content: Message text content.
101
+ media: Optional list of media URLs.
102
+ metadata: Optional channel-specific metadata.
103
+ """
104
+ if not self.is_allowed(sender_id):
105
+ return
106
+
107
+ msg = InboundMessage(
108
+ channel=self.name,
109
+ sender_id=str(sender_id),
110
+ chat_id=str(chat_id),
111
+ content=content,
112
+ media=media or [],
113
+ metadata=metadata or {}
114
+ )
115
+
116
+ await self.bus.publish_inbound(msg)
117
+
118
+ @property
119
+ def is_running(self) -> bool:
120
+ """Check if the channel is running."""
121
+ return self._running