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.
- kyber/__init__.py +6 -0
- kyber/__main__.py +8 -0
- kyber/agent/__init__.py +8 -0
- kyber/agent/context.py +224 -0
- kyber/agent/loop.py +687 -0
- kyber/agent/memory.py +109 -0
- kyber/agent/skills.py +244 -0
- kyber/agent/subagent.py +379 -0
- kyber/agent/tools/__init__.py +6 -0
- kyber/agent/tools/base.py +102 -0
- kyber/agent/tools/filesystem.py +191 -0
- kyber/agent/tools/message.py +86 -0
- kyber/agent/tools/registry.py +73 -0
- kyber/agent/tools/shell.py +141 -0
- kyber/agent/tools/spawn.py +65 -0
- kyber/agent/tools/task_status.py +53 -0
- kyber/agent/tools/web.py +163 -0
- kyber/bridge/package.json +26 -0
- kyber/bridge/src/index.ts +50 -0
- kyber/bridge/src/server.ts +104 -0
- kyber/bridge/src/types.d.ts +3 -0
- kyber/bridge/src/whatsapp.ts +185 -0
- kyber/bridge/tsconfig.json +16 -0
- kyber/bus/__init__.py +6 -0
- kyber/bus/events.py +37 -0
- kyber/bus/queue.py +81 -0
- kyber/channels/__init__.py +6 -0
- kyber/channels/base.py +121 -0
- kyber/channels/discord.py +304 -0
- kyber/channels/feishu.py +263 -0
- kyber/channels/manager.py +161 -0
- kyber/channels/telegram.py +302 -0
- kyber/channels/whatsapp.py +141 -0
- kyber/cli/__init__.py +1 -0
- kyber/cli/commands.py +736 -0
- kyber/config/__init__.py +6 -0
- kyber/config/loader.py +95 -0
- kyber/config/schema.py +205 -0
- kyber/cron/__init__.py +6 -0
- kyber/cron/service.py +346 -0
- kyber/cron/types.py +59 -0
- kyber/dashboard/__init__.py +5 -0
- kyber/dashboard/server.py +122 -0
- kyber/dashboard/static/app.js +458 -0
- kyber/dashboard/static/favicon.png +0 -0
- kyber/dashboard/static/index.html +107 -0
- kyber/dashboard/static/kyber_logo.png +0 -0
- kyber/dashboard/static/styles.css +608 -0
- kyber/heartbeat/__init__.py +5 -0
- kyber/heartbeat/service.py +130 -0
- kyber/providers/__init__.py +6 -0
- kyber/providers/base.py +69 -0
- kyber/providers/litellm_provider.py +227 -0
- kyber/providers/transcription.py +65 -0
- kyber/session/__init__.py +5 -0
- kyber/session/manager.py +202 -0
- kyber/skills/README.md +47 -0
- kyber/skills/github/SKILL.md +48 -0
- kyber/skills/skill-creator/SKILL.md +371 -0
- kyber/skills/summarize/SKILL.md +67 -0
- kyber/skills/tmux/SKILL.md +121 -0
- kyber/skills/tmux/scripts/find-sessions.sh +112 -0
- kyber/skills/tmux/scripts/wait-for-text.sh +83 -0
- kyber/skills/weather/SKILL.md +49 -0
- kyber/utils/__init__.py +5 -0
- kyber/utils/helpers.py +91 -0
- kyber_chat-1.0.0.dist-info/METADATA +35 -0
- kyber_chat-1.0.0.dist-info/RECORD +71 -0
- kyber_chat-1.0.0.dist-info/WHEEL +4 -0
- kyber_chat-1.0.0.dist-info/entry_points.txt +2 -0
- 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("&", "&").replace("<", "<").replace(">", ">")
|
|
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
|