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
kyber/agent/memory.py ADDED
@@ -0,0 +1,109 @@
1
+ """Memory system for persistent agent memory."""
2
+
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+
6
+ from kyber.utils.helpers import ensure_dir, today_date
7
+
8
+
9
+ class MemoryStore:
10
+ """
11
+ Memory system for the agent.
12
+
13
+ Supports daily notes (memory/YYYY-MM-DD.md) and long-term memory (MEMORY.md).
14
+ """
15
+
16
+ def __init__(self, workspace: Path):
17
+ self.workspace = workspace
18
+ self.memory_dir = ensure_dir(workspace / "memory")
19
+ self.memory_file = self.memory_dir / "MEMORY.md"
20
+
21
+ def get_today_file(self) -> Path:
22
+ """Get path to today's memory file."""
23
+ return self.memory_dir / f"{today_date()}.md"
24
+
25
+ def read_today(self) -> str:
26
+ """Read today's memory notes."""
27
+ today_file = self.get_today_file()
28
+ if today_file.exists():
29
+ return today_file.read_text(encoding="utf-8")
30
+ return ""
31
+
32
+ def append_today(self, content: str) -> None:
33
+ """Append content to today's memory notes."""
34
+ today_file = self.get_today_file()
35
+
36
+ if today_file.exists():
37
+ existing = today_file.read_text(encoding="utf-8")
38
+ content = existing + "\n" + content
39
+ else:
40
+ # Add header for new day
41
+ header = f"# {today_date()}\n\n"
42
+ content = header + content
43
+
44
+ today_file.write_text(content, encoding="utf-8")
45
+
46
+ def read_long_term(self) -> str:
47
+ """Read long-term memory (MEMORY.md)."""
48
+ if self.memory_file.exists():
49
+ return self.memory_file.read_text(encoding="utf-8")
50
+ return ""
51
+
52
+ def write_long_term(self, content: str) -> None:
53
+ """Write to long-term memory (MEMORY.md)."""
54
+ self.memory_file.write_text(content, encoding="utf-8")
55
+
56
+ def get_recent_memories(self, days: int = 7) -> str:
57
+ """
58
+ Get memories from the last N days.
59
+
60
+ Args:
61
+ days: Number of days to look back.
62
+
63
+ Returns:
64
+ Combined memory content.
65
+ """
66
+ from datetime import timedelta
67
+
68
+ memories = []
69
+ today = datetime.now().date()
70
+
71
+ for i in range(days):
72
+ date = today - timedelta(days=i)
73
+ date_str = date.strftime("%Y-%m-%d")
74
+ file_path = self.memory_dir / f"{date_str}.md"
75
+
76
+ if file_path.exists():
77
+ content = file_path.read_text(encoding="utf-8")
78
+ memories.append(content)
79
+
80
+ return "\n\n---\n\n".join(memories)
81
+
82
+ def list_memory_files(self) -> list[Path]:
83
+ """List all memory files sorted by date (newest first)."""
84
+ if not self.memory_dir.exists():
85
+ return []
86
+
87
+ files = list(self.memory_dir.glob("????-??-??.md"))
88
+ return sorted(files, reverse=True)
89
+
90
+ def get_memory_context(self) -> str:
91
+ """
92
+ Get memory context for the agent.
93
+
94
+ Returns:
95
+ Formatted memory context including long-term and recent memories.
96
+ """
97
+ parts = []
98
+
99
+ # Long-term memory
100
+ long_term = self.read_long_term()
101
+ if long_term:
102
+ parts.append("## Long-term Memory\n" + long_term)
103
+
104
+ # Today's notes
105
+ today = self.read_today()
106
+ if today:
107
+ parts.append("## Today's Notes\n" + today)
108
+
109
+ return "\n\n".join(parts) if parts else ""
kyber/agent/skills.py ADDED
@@ -0,0 +1,244 @@
1
+ """Skills loader for agent capabilities."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ # Default builtin skills directory (relative to this file)
10
+ BUILTIN_SKILLS_DIR = Path(__file__).parent.parent / "skills"
11
+
12
+ # Managed/local skills directory (user-installed, e.g. from ClawHub)
13
+ MANAGED_SKILLS_DIR = Path.home() / ".kyber" / "skills"
14
+
15
+ # Metadata namespace keys we understand (ours + OpenClaw-compatible)
16
+ _META_NAMESPACES = ("kyber", "openclaw")
17
+
18
+
19
+ class SkillsLoader:
20
+ """
21
+ Loader for agent skills.
22
+
23
+ Skills are markdown files (SKILL.md) that teach the agent how to use
24
+ specific tools or perform certain tasks.
25
+
26
+ Compatible with the AgentSkills / OpenClaw skill format.
27
+ Precedence: workspace > managed (~/.kyber/skills) > builtin.
28
+ """
29
+
30
+ def __init__(self, workspace: Path, builtin_skills_dir: Path | None = None):
31
+ self.workspace = workspace
32
+ self.workspace_skills = workspace / "skills"
33
+ self.managed_skills = MANAGED_SKILLS_DIR
34
+ self.builtin_skills = builtin_skills_dir or BUILTIN_SKILLS_DIR
35
+
36
+ def list_skills(self, filter_unavailable: bool = True) -> list[dict[str, str]]:
37
+ """
38
+ List all available skills.
39
+
40
+ Args:
41
+ filter_unavailable: If True, filter out skills with unmet requirements.
42
+
43
+ Returns:
44
+ List of skill info dicts with 'name', 'path', 'source'.
45
+ """
46
+ skills = []
47
+ seen = set()
48
+
49
+ def _scan(directory: Path, source: str):
50
+ if not directory.exists():
51
+ return
52
+ for skill_dir in directory.iterdir():
53
+ if skill_dir.is_dir() and skill_dir.name not in seen:
54
+ skill_file = skill_dir / "SKILL.md"
55
+ if skill_file.exists():
56
+ seen.add(skill_dir.name)
57
+ skills.append({"name": skill_dir.name, "path": str(skill_file), "source": source})
58
+
59
+ # Precedence: workspace > managed > builtin
60
+ _scan(self.workspace_skills, "workspace")
61
+ _scan(self.managed_skills, "managed")
62
+ if self.builtin_skills:
63
+ _scan(self.builtin_skills, "builtin")
64
+
65
+ # Filter by requirements
66
+ if filter_unavailable:
67
+ return [s for s in skills if self._check_requirements(self._get_skill_meta(s["name"]))]
68
+ return skills
69
+
70
+ def load_skill(self, name: str) -> str | None:
71
+ """
72
+ Load a skill by name.
73
+
74
+ Args:
75
+ name: Skill name (directory name).
76
+
77
+ Returns:
78
+ Skill content or None if not found.
79
+ """
80
+ # Precedence: workspace > managed > builtin
81
+ for base in (self.workspace_skills, self.managed_skills, self.builtin_skills):
82
+ if base:
83
+ skill_file = base / name / "SKILL.md"
84
+ if skill_file.exists():
85
+ return skill_file.read_text(encoding="utf-8")
86
+
87
+ return None
88
+
89
+ def load_skills_for_context(self, skill_names: list[str]) -> str:
90
+ """
91
+ Load specific skills for inclusion in agent context.
92
+
93
+ Args:
94
+ skill_names: List of skill names to load.
95
+
96
+ Returns:
97
+ Formatted skills content.
98
+ """
99
+ parts = []
100
+ for name in skill_names:
101
+ content = self.load_skill(name)
102
+ if content:
103
+ content = self._strip_frontmatter(content)
104
+ parts.append(f"### Skill: {name}\n\n{content}")
105
+
106
+ return "\n\n---\n\n".join(parts) if parts else ""
107
+
108
+ def build_skills_summary(self) -> str:
109
+ """
110
+ Build a summary of all skills (name, description, path, availability).
111
+
112
+ This is used for progressive loading - the agent can read the full
113
+ skill content using read_file when needed.
114
+
115
+ Returns:
116
+ XML-formatted skills summary.
117
+ """
118
+ all_skills = self.list_skills(filter_unavailable=False)
119
+ if not all_skills:
120
+ return ""
121
+
122
+ def escape_xml(s: str) -> str:
123
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
124
+
125
+ lines = ["<skills>"]
126
+ for s in all_skills:
127
+ name = escape_xml(s["name"])
128
+ path = s["path"]
129
+ desc = escape_xml(self._get_skill_description(s["name"]))
130
+ skill_meta = self._get_skill_meta(s["name"])
131
+ available = self._check_requirements(skill_meta)
132
+
133
+ lines.append(f" <skill available=\"{str(available).lower()}\">")
134
+ lines.append(f" <name>{name}</name>")
135
+ lines.append(f" <description>{desc}</description>")
136
+ lines.append(f" <location>{path}</location>")
137
+
138
+ # Show missing requirements for unavailable skills
139
+ if not available:
140
+ missing = self._get_missing_requirements(skill_meta)
141
+ if missing:
142
+ lines.append(f" <requires>{escape_xml(missing)}</requires>")
143
+
144
+ lines.append(f" </skill>")
145
+ lines.append("</skills>")
146
+
147
+ return "\n".join(lines)
148
+
149
+ def _get_missing_requirements(self, skill_meta: dict) -> str:
150
+ """Get a description of missing requirements."""
151
+ missing = []
152
+ requires = skill_meta.get("requires", {})
153
+ for b in requires.get("bins", []):
154
+ if not shutil.which(b):
155
+ missing.append(f"CLI: {b}")
156
+ for env in requires.get("env", []):
157
+ if not os.environ.get(env):
158
+ missing.append(f"ENV: {env}")
159
+ return ", ".join(missing)
160
+
161
+ def _get_skill_description(self, name: str) -> str:
162
+ """Get the description of a skill from its frontmatter."""
163
+ meta = self.get_skill_metadata(name)
164
+ if meta and meta.get("description"):
165
+ return meta["description"]
166
+ return name # Fallback to skill name
167
+
168
+ def _strip_frontmatter(self, content: str) -> str:
169
+ """Remove YAML frontmatter from markdown content."""
170
+ if content.startswith("---"):
171
+ match = re.match(r"^---\n.*?\n---\n", content, re.DOTALL)
172
+ if match:
173
+ return content[match.end():].strip()
174
+ return content
175
+
176
+ def _parse_kyber_metadata(self, raw: str) -> dict:
177
+ """Parse skill metadata JSON from frontmatter.
178
+
179
+ Understands both kyber and openclaw namespace keys so that
180
+ OpenClaw-format skills work out of the box.
181
+ """
182
+ try:
183
+ data = json.loads(raw)
184
+ if not isinstance(data, dict):
185
+ return {}
186
+ for ns in _META_NAMESPACES:
187
+ if ns in data:
188
+ return data[ns]
189
+ return {}
190
+ except (json.JSONDecodeError, TypeError):
191
+ return {}
192
+
193
+ def _check_requirements(self, skill_meta: dict) -> bool:
194
+ """Check if skill requirements are met (bins, env vars)."""
195
+ requires = skill_meta.get("requires", {})
196
+ for b in requires.get("bins", []):
197
+ if not shutil.which(b):
198
+ return False
199
+ for env in requires.get("env", []):
200
+ if not os.environ.get(env):
201
+ return False
202
+ return True
203
+
204
+ def _get_skill_meta(self, name: str) -> dict:
205
+ """Get kyber metadata for a skill (cached in frontmatter)."""
206
+ meta = self.get_skill_metadata(name) or {}
207
+ return self._parse_kyber_metadata(meta.get("metadata", ""))
208
+
209
+ def get_always_skills(self) -> list[str]:
210
+ """Get skills marked as always=true that meet requirements."""
211
+ result = []
212
+ for s in self.list_skills(filter_unavailable=True):
213
+ meta = self.get_skill_metadata(s["name"]) or {}
214
+ skill_meta = self._parse_kyber_metadata(meta.get("metadata", ""))
215
+ if skill_meta.get("always") or meta.get("always"):
216
+ result.append(s["name"])
217
+ return result
218
+
219
+ def get_skill_metadata(self, name: str) -> dict | None:
220
+ """
221
+ Get metadata from a skill's frontmatter.
222
+
223
+ Args:
224
+ name: Skill name.
225
+
226
+ Returns:
227
+ Metadata dict or None.
228
+ """
229
+ content = self.load_skill(name)
230
+ if not content:
231
+ return None
232
+
233
+ if content.startswith("---"):
234
+ match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
235
+ if match:
236
+ # Simple YAML parsing
237
+ metadata = {}
238
+ for line in match.group(1).split("\n"):
239
+ if ":" in line:
240
+ key, value = line.split(":", 1)
241
+ metadata[key.strip()] = value.strip().strip('"\'')
242
+ return metadata
243
+
244
+ return None