machinaos 0.0.1

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 (288) hide show
  1. package/.env.template +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/bin/cli.js +159 -0
  5. package/client/.dockerignore +45 -0
  6. package/client/Dockerfile +68 -0
  7. package/client/eslint.config.js +29 -0
  8. package/client/index.html +13 -0
  9. package/client/nginx.conf +66 -0
  10. package/client/package.json +48 -0
  11. package/client/src/App.tsx +27 -0
  12. package/client/src/Dashboard.tsx +1173 -0
  13. package/client/src/ParameterPanel.tsx +301 -0
  14. package/client/src/components/AIAgentNode.tsx +321 -0
  15. package/client/src/components/APIKeyValidator.tsx +118 -0
  16. package/client/src/components/ClaudeChatModelNode.tsx +18 -0
  17. package/client/src/components/ConditionalEdge.tsx +189 -0
  18. package/client/src/components/CredentialsModal.tsx +306 -0
  19. package/client/src/components/EdgeConditionEditor.tsx +443 -0
  20. package/client/src/components/GeminiChatModelNode.tsx +18 -0
  21. package/client/src/components/GenericNode.tsx +357 -0
  22. package/client/src/components/LocationParameterPanel.tsx +154 -0
  23. package/client/src/components/ModelNode.tsx +286 -0
  24. package/client/src/components/OpenAIChatModelNode.tsx +18 -0
  25. package/client/src/components/OutputPanel.tsx +471 -0
  26. package/client/src/components/ParameterRenderer.tsx +1874 -0
  27. package/client/src/components/SkillEditorModal.tsx +417 -0
  28. package/client/src/components/SquareNode.tsx +797 -0
  29. package/client/src/components/StartNode.tsx +250 -0
  30. package/client/src/components/ToolkitNode.tsx +365 -0
  31. package/client/src/components/TriggerNode.tsx +463 -0
  32. package/client/src/components/auth/LoginPage.tsx +247 -0
  33. package/client/src/components/auth/ProtectedRoute.tsx +59 -0
  34. package/client/src/components/base/BaseChatModelNode.tsx +271 -0
  35. package/client/src/components/icons/AIProviderIcons.tsx +50 -0
  36. package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
  37. package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
  38. package/client/src/components/maps/index.ts +26 -0
  39. package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
  40. package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
  41. package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
  42. package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
  43. package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
  44. package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
  45. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
  46. package/client/src/components/parameterPanel/index.ts +42 -0
  47. package/client/src/components/shared/DataPanel.tsx +142 -0
  48. package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
  49. package/client/src/components/ui/AIResultModal.tsx +204 -0
  50. package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
  51. package/client/src/components/ui/CodeEditor.tsx +81 -0
  52. package/client/src/components/ui/CollapsibleSection.tsx +88 -0
  53. package/client/src/components/ui/ComponentItem.tsx +154 -0
  54. package/client/src/components/ui/ComponentPalette.tsx +321 -0
  55. package/client/src/components/ui/ConsolePanel.tsx +1074 -0
  56. package/client/src/components/ui/ErrorBoundary.tsx +196 -0
  57. package/client/src/components/ui/InputNodesPanel.tsx +204 -0
  58. package/client/src/components/ui/MapSelector.tsx +314 -0
  59. package/client/src/components/ui/Modal.tsx +149 -0
  60. package/client/src/components/ui/NodeContextMenu.tsx +192 -0
  61. package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
  62. package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
  63. package/client/src/components/ui/SettingsPanel.tsx +243 -0
  64. package/client/src/components/ui/TopToolbar.tsx +736 -0
  65. package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
  66. package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
  67. package/client/src/config/antdTheme.ts +186 -0
  68. package/client/src/config/api.ts +54 -0
  69. package/client/src/contexts/AuthContext.tsx +221 -0
  70. package/client/src/contexts/ThemeContext.tsx +42 -0
  71. package/client/src/contexts/WebSocketContext.tsx +1971 -0
  72. package/client/src/factories/baseChatModelFactory.ts +256 -0
  73. package/client/src/hooks/useAndroidOperations.ts +164 -0
  74. package/client/src/hooks/useApiKeyValidation.ts +107 -0
  75. package/client/src/hooks/useApiKeys.ts +238 -0
  76. package/client/src/hooks/useAppTheme.ts +17 -0
  77. package/client/src/hooks/useComponentPalette.ts +51 -0
  78. package/client/src/hooks/useCopyPaste.ts +155 -0
  79. package/client/src/hooks/useDragAndDrop.ts +124 -0
  80. package/client/src/hooks/useDragVariable.ts +88 -0
  81. package/client/src/hooks/useExecution.ts +313 -0
  82. package/client/src/hooks/useParameterPanel.ts +176 -0
  83. package/client/src/hooks/useReactFlowNodes.ts +189 -0
  84. package/client/src/hooks/useToolSchema.ts +209 -0
  85. package/client/src/hooks/useWhatsApp.ts +196 -0
  86. package/client/src/hooks/useWorkflowManagement.ts +46 -0
  87. package/client/src/index.css +315 -0
  88. package/client/src/main.tsx +19 -0
  89. package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
  90. package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
  91. package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
  92. package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
  93. package/client/src/nodeDefinitions/chatNodes.ts +135 -0
  94. package/client/src/nodeDefinitions/codeNodes.ts +54 -0
  95. package/client/src/nodeDefinitions/documentNodes.ts +379 -0
  96. package/client/src/nodeDefinitions/index.ts +15 -0
  97. package/client/src/nodeDefinitions/locationNodes.ts +463 -0
  98. package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
  99. package/client/src/nodeDefinitions/skillNodes.ts +211 -0
  100. package/client/src/nodeDefinitions/toolNodes.ts +198 -0
  101. package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
  102. package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
  103. package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
  104. package/client/src/nodeDefinitions.ts +104 -0
  105. package/client/src/schemas/workflowSchema.ts +264 -0
  106. package/client/src/services/dynamicParameterService.ts +96 -0
  107. package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
  108. package/client/src/services/executionService.ts +232 -0
  109. package/client/src/services/workflowApi.ts +91 -0
  110. package/client/src/store/useAppStore.ts +582 -0
  111. package/client/src/styles/theme.ts +508 -0
  112. package/client/src/styles/zIndex.ts +17 -0
  113. package/client/src/types/ComponentTypes.ts +39 -0
  114. package/client/src/types/EdgeCondition.ts +231 -0
  115. package/client/src/types/INodeProperties.ts +288 -0
  116. package/client/src/types/NodeTypes.ts +28 -0
  117. package/client/src/utils/formatters.ts +33 -0
  118. package/client/src/utils/googleMapsLoader.ts +140 -0
  119. package/client/src/utils/locationUtils.ts +85 -0
  120. package/client/src/utils/nodeUtils.ts +31 -0
  121. package/client/src/utils/workflow.ts +30 -0
  122. package/client/src/utils/workflowExport.ts +120 -0
  123. package/client/src/vite-env.d.ts +12 -0
  124. package/client/tailwind.config.js +60 -0
  125. package/client/tsconfig.json +25 -0
  126. package/client/tsconfig.node.json +11 -0
  127. package/client/vite.config.js +35 -0
  128. package/docker-compose.prod.yml +107 -0
  129. package/docker-compose.yml +104 -0
  130. package/docs-MachinaOs/README.md +85 -0
  131. package/docs-MachinaOs/deployment/docker.mdx +228 -0
  132. package/docs-MachinaOs/deployment/production.mdx +345 -0
  133. package/docs-MachinaOs/docs.json +75 -0
  134. package/docs-MachinaOs/faq.mdx +309 -0
  135. package/docs-MachinaOs/favicon.svg +5 -0
  136. package/docs-MachinaOs/installation.mdx +160 -0
  137. package/docs-MachinaOs/introduction.mdx +114 -0
  138. package/docs-MachinaOs/logo/dark.svg +6 -0
  139. package/docs-MachinaOs/logo/light.svg +6 -0
  140. package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
  141. package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
  142. package/docs-MachinaOs/nodes/android.mdx +411 -0
  143. package/docs-MachinaOs/nodes/overview.mdx +181 -0
  144. package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
  145. package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
  146. package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
  147. package/docs-MachinaOs/quickstart.mdx +119 -0
  148. package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
  149. package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
  150. package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
  151. package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
  152. package/nul +0 -0
  153. package/package.json +70 -0
  154. package/scripts/build.js +158 -0
  155. package/scripts/check-ports.ps1 +33 -0
  156. package/scripts/clean.js +40 -0
  157. package/scripts/docker.js +93 -0
  158. package/scripts/kill-port.ps1 +154 -0
  159. package/scripts/start.js +210 -0
  160. package/scripts/stop.js +325 -0
  161. package/server/.dockerignore +44 -0
  162. package/server/Dockerfile +45 -0
  163. package/server/constants.py +249 -0
  164. package/server/core/__init__.py +1 -0
  165. package/server/core/cache.py +461 -0
  166. package/server/core/config.py +128 -0
  167. package/server/core/container.py +99 -0
  168. package/server/core/database.py +1211 -0
  169. package/server/core/logging.py +314 -0
  170. package/server/main.py +289 -0
  171. package/server/middleware/__init__.py +5 -0
  172. package/server/middleware/auth.py +89 -0
  173. package/server/models/__init__.py +1 -0
  174. package/server/models/auth.py +52 -0
  175. package/server/models/cache.py +24 -0
  176. package/server/models/database.py +211 -0
  177. package/server/models/nodes.py +455 -0
  178. package/server/package.json +9 -0
  179. package/server/pyproject.toml +72 -0
  180. package/server/requirements.txt +83 -0
  181. package/server/routers/__init__.py +1 -0
  182. package/server/routers/android.py +294 -0
  183. package/server/routers/auth.py +203 -0
  184. package/server/routers/database.py +151 -0
  185. package/server/routers/maps.py +142 -0
  186. package/server/routers/nodejs_compat.py +289 -0
  187. package/server/routers/webhook.py +90 -0
  188. package/server/routers/websocket.py +2127 -0
  189. package/server/routers/whatsapp.py +761 -0
  190. package/server/routers/workflow.py +200 -0
  191. package/server/services/__init__.py +1 -0
  192. package/server/services/ai.py +2415 -0
  193. package/server/services/android/__init__.py +27 -0
  194. package/server/services/android/broadcaster.py +114 -0
  195. package/server/services/android/client.py +608 -0
  196. package/server/services/android/manager.py +78 -0
  197. package/server/services/android/protocol.py +165 -0
  198. package/server/services/android_service.py +588 -0
  199. package/server/services/auth.py +131 -0
  200. package/server/services/chat_client.py +160 -0
  201. package/server/services/deployment/__init__.py +12 -0
  202. package/server/services/deployment/manager.py +706 -0
  203. package/server/services/deployment/state.py +47 -0
  204. package/server/services/deployment/triggers.py +275 -0
  205. package/server/services/event_waiter.py +785 -0
  206. package/server/services/execution/__init__.py +77 -0
  207. package/server/services/execution/cache.py +769 -0
  208. package/server/services/execution/conditions.py +373 -0
  209. package/server/services/execution/dlq.py +132 -0
  210. package/server/services/execution/executor.py +1351 -0
  211. package/server/services/execution/models.py +531 -0
  212. package/server/services/execution/recovery.py +235 -0
  213. package/server/services/handlers/__init__.py +126 -0
  214. package/server/services/handlers/ai.py +355 -0
  215. package/server/services/handlers/android.py +260 -0
  216. package/server/services/handlers/code.py +278 -0
  217. package/server/services/handlers/document.py +598 -0
  218. package/server/services/handlers/http.py +193 -0
  219. package/server/services/handlers/polyglot.py +105 -0
  220. package/server/services/handlers/tools.py +845 -0
  221. package/server/services/handlers/triggers.py +107 -0
  222. package/server/services/handlers/utility.py +822 -0
  223. package/server/services/handlers/whatsapp.py +476 -0
  224. package/server/services/maps.py +289 -0
  225. package/server/services/memory_store.py +103 -0
  226. package/server/services/node_executor.py +375 -0
  227. package/server/services/parameter_resolver.py +218 -0
  228. package/server/services/polyglot_client.py +169 -0
  229. package/server/services/scheduler.py +155 -0
  230. package/server/services/skill_loader.py +417 -0
  231. package/server/services/status_broadcaster.py +826 -0
  232. package/server/services/temporal/__init__.py +23 -0
  233. package/server/services/temporal/activities.py +344 -0
  234. package/server/services/temporal/client.py +76 -0
  235. package/server/services/temporal/executor.py +147 -0
  236. package/server/services/temporal/worker.py +251 -0
  237. package/server/services/temporal/workflow.py +355 -0
  238. package/server/services/temporal/ws_client.py +236 -0
  239. package/server/services/text.py +111 -0
  240. package/server/services/user_auth.py +172 -0
  241. package/server/services/websocket_client.py +29 -0
  242. package/server/services/workflow.py +597 -0
  243. package/server/skills/android-skill/SKILL.md +82 -0
  244. package/server/skills/assistant-personality/SKILL.md +45 -0
  245. package/server/skills/code-skill/SKILL.md +140 -0
  246. package/server/skills/http-skill/SKILL.md +161 -0
  247. package/server/skills/maps-skill/SKILL.md +170 -0
  248. package/server/skills/memory-skill/SKILL.md +154 -0
  249. package/server/skills/scheduler-skill/SKILL.md +84 -0
  250. package/server/skills/whatsapp-skill/SKILL.md +283 -0
  251. package/server/uv.lock +2916 -0
  252. package/server/whatsapp-rpc/.dockerignore +30 -0
  253. package/server/whatsapp-rpc/Dockerfile +44 -0
  254. package/server/whatsapp-rpc/Dockerfile.web +17 -0
  255. package/server/whatsapp-rpc/README.md +139 -0
  256. package/server/whatsapp-rpc/cli.js +95 -0
  257. package/server/whatsapp-rpc/configs/config.yaml +7 -0
  258. package/server/whatsapp-rpc/docker-compose.yml +35 -0
  259. package/server/whatsapp-rpc/docs/API.md +410 -0
  260. package/server/whatsapp-rpc/go.mod +67 -0
  261. package/server/whatsapp-rpc/go.sum +203 -0
  262. package/server/whatsapp-rpc/package.json +30 -0
  263. package/server/whatsapp-rpc/schema.json +1294 -0
  264. package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
  265. package/server/whatsapp-rpc/scripts/cli.js +162 -0
  266. package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
  267. package/server/whatsapp-rpc/src/go/config/config.go +49 -0
  268. package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
  269. package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
  270. package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
  271. package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
  272. package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
  273. package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
  274. package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
  275. package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
  276. package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
  277. package/server/whatsapp-rpc/web/app.py +609 -0
  278. package/server/whatsapp-rpc/web/requirements.txt +6 -0
  279. package/server/whatsapp-rpc/web/rpc_client.py +427 -0
  280. package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
  281. package/server/whatsapp-rpc/web/templates/base.html +150 -0
  282. package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
  283. package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
  284. package/server/whatsapp-rpc/web/templates/groups.html +328 -0
  285. package/server/whatsapp-rpc/web/templates/messages.html +465 -0
  286. package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
  287. package/server/whatsapp-rpc/web/templates/send.html +259 -0
  288. package/server/whatsapp-rpc/web/templates/settings.html +459 -0
@@ -0,0 +1,417 @@
1
+ """Skill loader service for discovering and loading Agent Skills.
2
+
3
+ Implements the Agent Skills specification (https://agentskills.io/specification).
4
+ Skills are modular capabilities defined in Markdown files that the Chat Agent
5
+ can discover and use on demand.
6
+ """
7
+
8
+ import re
9
+ import yaml
10
+ from pathlib import Path
11
+ from dataclasses import dataclass, field
12
+ from typing import Dict, List, Any, Optional
13
+ from core.logging import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class SkillMetadata:
20
+ """Metadata from SKILL.md frontmatter (loaded at startup for all skills)."""
21
+ name: str
22
+ description: str
23
+ allowed_tools: List[str] = field(default_factory=list)
24
+ metadata: Dict[str, Any] = field(default_factory=dict)
25
+ path: Optional[Path] = None # Path to skill directory
26
+
27
+
28
+ @dataclass
29
+ class Skill:
30
+ """Full skill content (loaded on-demand when activated)."""
31
+ metadata: SkillMetadata
32
+ instructions: str # Markdown body after frontmatter
33
+ scripts: Dict[str, str] = field(default_factory=dict) # filename -> content
34
+ references: Dict[str, str] = field(default_factory=dict) # filename -> content
35
+ assets: Dict[str, bytes] = field(default_factory=dict) # filename -> binary content
36
+
37
+
38
+ class SkillLoader:
39
+ """Loads and manages Agent Skills from filesystem and database.
40
+
41
+ Skills are loaded from multiple directories with priority:
42
+ 1. Built-in skills: server/skills/
43
+ 2. Project skills: .machina/skills/ (in current directory)
44
+ 3. User skills from database (created via UI)
45
+
46
+ Follows progressive disclosure pattern:
47
+ - scan_skills(): Load only metadata (~100 tokens per skill)
48
+ - load_skill(): Load full content when activated (~5000 tokens max)
49
+ """
50
+
51
+ def __init__(self, skill_dirs: List[Path] = None, database=None):
52
+ """Initialize skill loader.
53
+
54
+ Args:
55
+ skill_dirs: List of directories to scan for skills
56
+ database: Database instance for loading user-created skills
57
+ """
58
+ self._skill_dirs = skill_dirs or []
59
+ self._database = database
60
+ self._registry: Dict[str, SkillMetadata] = {}
61
+ self._cache: Dict[str, Skill] = {} # Cache loaded skills
62
+
63
+ def scan_skills(self) -> Dict[str, SkillMetadata]:
64
+ """Scan all skill directories and load metadata.
65
+
66
+ Returns:
67
+ Dict mapping skill name to SkillMetadata
68
+ """
69
+ self._registry.clear()
70
+
71
+ # Scan filesystem directories
72
+ for skill_dir in self._skill_dirs:
73
+ if not skill_dir.exists():
74
+ logger.debug(f"[SkillLoader] Skill directory not found: {skill_dir}")
75
+ continue
76
+
77
+ for skill_path in skill_dir.iterdir():
78
+ if skill_path.is_dir():
79
+ skill_md = skill_path / "SKILL.md"
80
+ if skill_md.exists():
81
+ try:
82
+ metadata = self._parse_skill_metadata(skill_md)
83
+ if metadata:
84
+ metadata.path = skill_path
85
+ self._registry[metadata.name] = metadata
86
+ logger.debug(f"[SkillLoader] Loaded skill: {metadata.name}")
87
+ except Exception as e:
88
+ logger.error(f"[SkillLoader] Failed to parse {skill_md}: {e}")
89
+
90
+ logger.info(f"[SkillLoader] Loaded {len(self._registry)} skills from filesystem")
91
+ return self._registry
92
+
93
+ async def scan_skills_with_database(self) -> Dict[str, SkillMetadata]:
94
+ """Scan skills from filesystem and database.
95
+
96
+ Returns:
97
+ Dict mapping skill name to SkillMetadata
98
+ """
99
+ # First scan filesystem
100
+ self.scan_skills()
101
+
102
+ # Then load from database (overrides filesystem if same name)
103
+ if self._database:
104
+ try:
105
+ user_skills = await self._database.get_all_user_skills()
106
+ for skill in user_skills:
107
+ allowed_tools = []
108
+ if skill.allowed_tools:
109
+ allowed_tools = [t.strip() for t in skill.allowed_tools.split(',')]
110
+
111
+ metadata_dict = {}
112
+ if skill.metadata_json:
113
+ import json
114
+ metadata_dict = json.loads(skill.metadata_json)
115
+
116
+ self._registry[skill.name] = SkillMetadata(
117
+ name=skill.name,
118
+ description=skill.description,
119
+ allowed_tools=allowed_tools,
120
+ metadata=metadata_dict,
121
+ path=None # Database skills have no path
122
+ )
123
+ logger.info(f"[SkillLoader] Loaded {len(user_skills)} skills from database")
124
+ except Exception as e:
125
+ logger.error(f"[SkillLoader] Failed to load skills from database: {e}")
126
+
127
+ return self._registry
128
+
129
+ def _parse_skill_metadata(self, skill_md_path: Path) -> Optional[SkillMetadata]:
130
+ """Parse SKILL.md frontmatter to extract metadata.
131
+
132
+ Args:
133
+ skill_md_path: Path to SKILL.md file
134
+
135
+ Returns:
136
+ SkillMetadata or None if parsing fails
137
+ """
138
+ content = skill_md_path.read_text(encoding='utf-8')
139
+
140
+ # Parse YAML frontmatter (between --- markers)
141
+ frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
142
+ if not frontmatter_match:
143
+ logger.warning(f"[SkillLoader] No frontmatter in {skill_md_path}")
144
+ return None
145
+
146
+ try:
147
+ frontmatter = yaml.safe_load(frontmatter_match.group(1))
148
+ except yaml.YAMLError as e:
149
+ logger.error(f"[SkillLoader] Invalid YAML in {skill_md_path}: {e}")
150
+ return None
151
+
152
+ # Validate required fields
153
+ name = frontmatter.get('name')
154
+ description = frontmatter.get('description')
155
+
156
+ if not name or not description:
157
+ logger.warning(f"[SkillLoader] Missing name or description in {skill_md_path}")
158
+ return None
159
+
160
+ # Validate name format (lowercase, hyphens, no consecutive hyphens)
161
+ if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', name):
162
+ logger.warning(f"[SkillLoader] Invalid skill name format: {name}")
163
+ return None
164
+
165
+ # Parse allowed-tools (space-delimited)
166
+ allowed_tools = []
167
+ if 'allowed-tools' in frontmatter:
168
+ allowed_tools = frontmatter['allowed-tools'].split()
169
+
170
+ return SkillMetadata(
171
+ name=name,
172
+ description=description,
173
+ allowed_tools=allowed_tools,
174
+ metadata=frontmatter.get('metadata', {})
175
+ )
176
+
177
+ def load_skill(self, name: str) -> Optional[Skill]:
178
+ """Load full skill content by name.
179
+
180
+ Args:
181
+ name: Skill name to load
182
+
183
+ Returns:
184
+ Skill with full content or None if not found
185
+ """
186
+ # Check cache first
187
+ if name in self._cache:
188
+ return self._cache[name]
189
+
190
+ # Check registry
191
+ if name not in self._registry:
192
+ logger.warning(f"[SkillLoader] Skill not found: {name}")
193
+ return None
194
+
195
+ metadata = self._registry[name]
196
+
197
+ # Database skills have no path - load from database
198
+ if metadata.path is None:
199
+ return self._load_skill_from_database(name, metadata)
200
+
201
+ # Filesystem skills
202
+ skill_path = metadata.path
203
+ skill_md = skill_path / "SKILL.md"
204
+
205
+ if not skill_md.exists():
206
+ logger.error(f"[SkillLoader] SKILL.md not found for {name}")
207
+ return None
208
+
209
+ # Parse full content
210
+ content = skill_md.read_text(encoding='utf-8')
211
+
212
+ # Extract body (after frontmatter)
213
+ frontmatter_match = re.match(r'^---\s*\n.*?\n---\s*\n', content, re.DOTALL)
214
+ if frontmatter_match:
215
+ instructions = content[frontmatter_match.end():]
216
+ else:
217
+ instructions = content
218
+
219
+ # Load scripts directory
220
+ scripts = {}
221
+ scripts_dir = skill_path / "scripts"
222
+ if scripts_dir.exists():
223
+ for script_file in scripts_dir.iterdir():
224
+ if script_file.is_file():
225
+ try:
226
+ scripts[script_file.name] = script_file.read_text(encoding='utf-8')
227
+ except Exception as e:
228
+ logger.warning(f"[SkillLoader] Failed to read script {script_file}: {e}")
229
+
230
+ # Load references directory
231
+ references = {}
232
+ refs_dir = skill_path / "references"
233
+ if refs_dir.exists():
234
+ for ref_file in refs_dir.iterdir():
235
+ if ref_file.is_file() and ref_file.suffix in ['.md', '.txt', '.json']:
236
+ try:
237
+ references[ref_file.name] = ref_file.read_text(encoding='utf-8')
238
+ except Exception as e:
239
+ logger.warning(f"[SkillLoader] Failed to read reference {ref_file}: {e}")
240
+
241
+ skill = Skill(
242
+ metadata=metadata,
243
+ instructions=instructions,
244
+ scripts=scripts,
245
+ references=references
246
+ )
247
+
248
+ # Cache the loaded skill
249
+ self._cache[name] = skill
250
+ logger.debug(f"[SkillLoader] Loaded full skill: {name} ({len(instructions)} chars)")
251
+
252
+ return skill
253
+
254
+ def _load_skill_from_database(self, name: str, metadata: SkillMetadata) -> Optional[Skill]:
255
+ """Load skill from database (synchronous wrapper for cached data)."""
256
+ # This should be called after scan_skills_with_database
257
+ # The full instructions should be loaded via async method
258
+ logger.warning(f"[SkillLoader] Database skill {name} requires async loading")
259
+ return None
260
+
261
+ async def load_skill_async(self, name: str) -> Optional[Skill]:
262
+ """Async version of load_skill for database skills.
263
+
264
+ Args:
265
+ name: Skill name to load
266
+
267
+ Returns:
268
+ Skill with full content or None if not found
269
+ """
270
+ # Check cache first
271
+ if name in self._cache:
272
+ return self._cache[name]
273
+
274
+ # Try filesystem first
275
+ if name in self._registry and self._registry[name].path is not None:
276
+ return self.load_skill(name)
277
+
278
+ # Load from database
279
+ if self._database:
280
+ try:
281
+ user_skill = await self._database.get_user_skill_by_name(name)
282
+ if user_skill:
283
+ allowed_tools = []
284
+ if user_skill.allowed_tools:
285
+ allowed_tools = [t.strip() for t in user_skill.allowed_tools.split(',')]
286
+
287
+ metadata_dict = {}
288
+ if user_skill.metadata_json:
289
+ import json
290
+ metadata_dict = json.loads(user_skill.metadata_json)
291
+
292
+ metadata = SkillMetadata(
293
+ name=user_skill.name,
294
+ description=user_skill.description,
295
+ allowed_tools=allowed_tools,
296
+ metadata=metadata_dict,
297
+ path=None
298
+ )
299
+
300
+ skill = Skill(
301
+ metadata=metadata,
302
+ instructions=user_skill.instructions,
303
+ scripts={},
304
+ references={}
305
+ )
306
+
307
+ self._cache[name] = skill
308
+ return skill
309
+ except Exception as e:
310
+ logger.error(f"[SkillLoader] Failed to load skill {name} from database: {e}")
311
+
312
+ return None
313
+
314
+ def get_registry_prompt(self, skill_names: List[str] = None) -> str:
315
+ """Generate skill registry for LLM system prompt.
316
+
317
+ Args:
318
+ skill_names: Optional list of skill names to include.
319
+ If None, includes all registered skills.
320
+
321
+ Returns:
322
+ Formatted string listing available skills
323
+ """
324
+ skills_to_include = skill_names or list(self._registry.keys())
325
+
326
+ if not skills_to_include:
327
+ return ""
328
+
329
+ lines = ["## Available Skills", ""]
330
+ lines.append("You have access to the following skills. When a user's request matches a skill's purpose, activate it to help them.")
331
+ lines.append("")
332
+
333
+ for name in skills_to_include:
334
+ if name in self._registry:
335
+ metadata = self._registry[name]
336
+ lines.append(f"- **{name}**: {metadata.description}")
337
+ if metadata.allowed_tools:
338
+ tools_str = ", ".join(metadata.allowed_tools)
339
+ lines.append(f" - Tools: {tools_str}")
340
+
341
+ lines.append("")
342
+ lines.append("To use a skill, identify when the user's request matches its purpose and apply the skill's instructions.")
343
+
344
+ return "\n".join(lines)
345
+
346
+ def get_skill_instructions(self, name: str) -> Optional[str]:
347
+ """Get full instructions for a skill (loads if not cached).
348
+
349
+ Args:
350
+ name: Skill name
351
+
352
+ Returns:
353
+ Markdown instructions or None if not found
354
+ """
355
+ skill = self.load_skill(name)
356
+ return skill.instructions if skill else None
357
+
358
+ def get_available_skills(self) -> List[Dict[str, Any]]:
359
+ """Get list of available skills for frontend display.
360
+
361
+ Returns:
362
+ List of skill info dicts
363
+ """
364
+ return [
365
+ {
366
+ "name": metadata.name,
367
+ "description": metadata.description,
368
+ "allowed_tools": metadata.allowed_tools,
369
+ "metadata": metadata.metadata,
370
+ "is_builtin": metadata.path is not None
371
+ }
372
+ for metadata in self._registry.values()
373
+ ]
374
+
375
+ def clear_cache(self):
376
+ """Clear the skill cache."""
377
+ self._cache.clear()
378
+ logger.debug("[SkillLoader] Cache cleared")
379
+
380
+
381
+ # Global skill loader instance
382
+ _skill_loader: Optional[SkillLoader] = None
383
+
384
+
385
+ def get_skill_loader() -> SkillLoader:
386
+ """Get the global skill loader instance."""
387
+ global _skill_loader
388
+ if _skill_loader is None:
389
+ # Default directories
390
+ server_dir = Path(__file__).parent.parent
391
+ skill_dirs = [
392
+ server_dir / "skills", # Built-in skills
393
+ Path.cwd() / ".machina" / "skills", # Project skills
394
+ ]
395
+ _skill_loader = SkillLoader(skill_dirs=skill_dirs)
396
+ _skill_loader.scan_skills()
397
+ return _skill_loader
398
+
399
+
400
+ def init_skill_loader(database=None) -> SkillLoader:
401
+ """Initialize the global skill loader with database support.
402
+
403
+ Args:
404
+ database: Database instance for user skill storage
405
+
406
+ Returns:
407
+ Initialized SkillLoader
408
+ """
409
+ global _skill_loader
410
+ server_dir = Path(__file__).parent.parent
411
+ skill_dirs = [
412
+ server_dir / "skills", # Built-in skills
413
+ Path.cwd() / ".machina" / "skills", # Project skills
414
+ ]
415
+ _skill_loader = SkillLoader(skill_dirs=skill_dirs, database=database)
416
+ _skill_loader.scan_skills()
417
+ return _skill_loader