flowent 0.0.12 → 0.1.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 (54) hide show
  1. package/backend/pyproject.toml +2 -1
  2. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  3. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  4. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  5. package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/agent.py +28 -7
  20. package/backend/src/flowent/channels.py +296 -0
  21. package/backend/src/flowent/main.py +226 -3
  22. package/backend/src/flowent/mcp.py +484 -0
  23. package/backend/src/flowent/mcp_import.py +202 -0
  24. package/backend/src/flowent/skills.py +157 -0
  25. package/backend/src/flowent/static/assets/index-DqTHSMBo.js +81 -0
  26. package/backend/src/flowent/static/assets/index-d3FBbOXX.css +2 -0
  27. package/backend/src/flowent/static/index.html +2 -2
  28. package/backend/src/flowent/storage.py +419 -0
  29. package/backend/src/flowent/tools.py +34 -7
  30. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  31. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  32. package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
  33. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  34. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  35. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  36. package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
  37. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  38. package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
  39. package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
  40. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  41. package/backend/tests/test_agent_tools.py +54 -0
  42. package/backend/tests/test_channels.py +360 -0
  43. package/backend/tests/test_mcp.py +710 -0
  44. package/backend/tests/test_persistence.py +30 -0
  45. package/backend/tests/test_skills.py +462 -0
  46. package/backend/uv.lock +160 -1
  47. package/dist/frontend/assets/index-DqTHSMBo.js +81 -0
  48. package/dist/frontend/assets/index-d3FBbOXX.css +2 -0
  49. package/dist/frontend/index.html +2 -2
  50. package/package.json +1 -1
  51. package/backend/src/flowent/static/assets/index-BwQOML_0.css +0 -2
  52. package/backend/src/flowent/static/assets/index-DXQ_smj0.js +0 -81
  53. package/dist/frontend/assets/index-BwQOML_0.css +0 -2
  54. package/dist/frontend/assets/index-DXQ_smj0.js +0 -81
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from flowent.llm import ChatMessage
8
+ from flowent.storage import StateStore, StoredSkill
9
+
10
+ PROJECT_SKILLS_DIRECTORY = Path(".flowent") / "skills"
11
+ AGENTS_SKILLS_DIRECTORY = Path(".agents") / "skills"
12
+ SKILL_FILENAME = "SKILL.md"
13
+ SKILL_REFERENCE_PATTERN = re.compile(r"(?<!\w)\$([a-z0-9][a-z0-9-]*)\b")
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class SkillDocument:
18
+ body: str
19
+ skill: StoredSkill
20
+
21
+
22
+ def skill_slug(value: str) -> str:
23
+ slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
24
+ return slug or "skill"
25
+
26
+
27
+ def skill_id(scope: str, path: Path) -> str:
28
+ return f"{scope}:{path.resolve(strict=False)}"
29
+
30
+
31
+ def skill_directories(cwd: Path, store: StateStore) -> list[tuple[str, Path]]:
32
+ return [
33
+ ("project", cwd.resolve(strict=False) / PROJECT_SKILLS_DIRECTORY),
34
+ ("project", cwd.resolve(strict=False) / AGENTS_SKILLS_DIRECTORY),
35
+ ("user", store.directory / "skills"),
36
+ ]
37
+
38
+
39
+ def parse_skill_frontmatter(content: str) -> tuple[dict[str, str], str]:
40
+ if not content.startswith("---\n"):
41
+ return {}, content
42
+
43
+ end_index = content.find("\n---", 4)
44
+ if end_index == -1:
45
+ return {}, content
46
+
47
+ metadata: dict[str, str] = {}
48
+ for line in content[4:end_index].splitlines():
49
+ key, separator, value = line.partition(":")
50
+ if not separator:
51
+ continue
52
+ metadata[key.strip().lower()] = value.strip().strip("\"'")
53
+
54
+ body_start = end_index + len("\n---")
55
+ if content[body_start : body_start + 1] == "\n":
56
+ body_start += 1
57
+ return metadata, content[body_start:]
58
+
59
+
60
+ def load_skill_document(scope: str, path: Path, enabled: bool) -> SkillDocument:
61
+ content = path.read_text(errors="replace")
62
+ metadata, body = parse_skill_frontmatter(content)
63
+ name = metadata.get("name", "").strip()
64
+ description = metadata.get("description", "").strip()
65
+ fallback_name = path.parent.name.replace("-", " ").strip().title() or "Skill"
66
+ display_name = name or fallback_name
67
+ error = "" if name and description else "Skill needs a name and description."
68
+ slug = skill_slug(display_name)
69
+ return SkillDocument(
70
+ body=body.strip(),
71
+ skill=StoredSkill(
72
+ description=description,
73
+ enabled=enabled,
74
+ error=error,
75
+ id=skill_id(scope, path),
76
+ name=display_name,
77
+ path=str(path.resolve(strict=False)),
78
+ scope=scope,
79
+ slug=slug,
80
+ ),
81
+ )
82
+
83
+
84
+ def discover_skill_documents(cwd: Path, store: StateStore) -> list[SkillDocument]:
85
+ enabled_by_id = store.read_skill_enabled()
86
+ documents: list[SkillDocument] = []
87
+ for scope, directory in skill_directories(cwd, store):
88
+ if not directory.is_dir():
89
+ continue
90
+ for path in sorted(directory.glob(f"*/{SKILL_FILENAME}")):
91
+ resolved_id = skill_id(scope, path)
92
+ documents.append(
93
+ load_skill_document(
94
+ scope,
95
+ path,
96
+ enabled_by_id.get(resolved_id, True),
97
+ )
98
+ )
99
+ return documents
100
+
101
+
102
+ def discover_skills(cwd: Path, store: StateStore) -> list[StoredSkill]:
103
+ return [document.skill for document in discover_skill_documents(cwd, store)]
104
+
105
+
106
+ def referenced_skill_slugs(content: str) -> list[str]:
107
+ slugs: list[str] = []
108
+ for match in SKILL_REFERENCE_PATTERN.finditer(content):
109
+ slug = match.group(1)
110
+ if slug not in slugs:
111
+ slugs.append(slug)
112
+ return slugs
113
+
114
+
115
+ def explicit_skill_messages(
116
+ cwd: Path,
117
+ store: StateStore,
118
+ content: str,
119
+ ) -> list[ChatMessage]:
120
+ requested_slugs = referenced_skill_slugs(content)
121
+ if not requested_slugs:
122
+ return []
123
+
124
+ documents_by_slug = {
125
+ document.skill.slug: document
126
+ for document in discover_skill_documents(cwd, store)
127
+ if document.skill.enabled and not document.skill.error
128
+ }
129
+ messages: list[ChatMessage] = []
130
+ for slug in requested_slugs:
131
+ document = documents_by_slug.get(slug)
132
+ if document is None:
133
+ continue
134
+ messages.append(
135
+ ChatMessage(
136
+ role="user",
137
+ content=(
138
+ f'<skill name="{document.skill.name}" slug="{document.skill.slug}">\n'
139
+ f"{document.body}\n"
140
+ "</skill>"
141
+ ),
142
+ )
143
+ )
144
+ return messages
145
+
146
+
147
+ def update_skill_enabled(
148
+ cwd: Path,
149
+ store: StateStore,
150
+ skill_id_value: str,
151
+ enabled: bool,
152
+ ) -> StoredSkill:
153
+ for skill in discover_skills(cwd, store):
154
+ if skill.id == skill_id_value:
155
+ store.save_skill_enabled(skill_id_value, enabled)
156
+ return skill.model_copy(update={"enabled": enabled})
157
+ raise KeyError(skill_id_value)