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.
- package/.env.template +71 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/cli.js +159 -0
- package/client/.dockerignore +45 -0
- package/client/Dockerfile +68 -0
- package/client/eslint.config.js +29 -0
- package/client/index.html +13 -0
- package/client/nginx.conf +66 -0
- package/client/package.json +48 -0
- package/client/src/App.tsx +27 -0
- package/client/src/Dashboard.tsx +1173 -0
- package/client/src/ParameterPanel.tsx +301 -0
- package/client/src/components/AIAgentNode.tsx +321 -0
- package/client/src/components/APIKeyValidator.tsx +118 -0
- package/client/src/components/ClaudeChatModelNode.tsx +18 -0
- package/client/src/components/ConditionalEdge.tsx +189 -0
- package/client/src/components/CredentialsModal.tsx +306 -0
- package/client/src/components/EdgeConditionEditor.tsx +443 -0
- package/client/src/components/GeminiChatModelNode.tsx +18 -0
- package/client/src/components/GenericNode.tsx +357 -0
- package/client/src/components/LocationParameterPanel.tsx +154 -0
- package/client/src/components/ModelNode.tsx +286 -0
- package/client/src/components/OpenAIChatModelNode.tsx +18 -0
- package/client/src/components/OutputPanel.tsx +471 -0
- package/client/src/components/ParameterRenderer.tsx +1874 -0
- package/client/src/components/SkillEditorModal.tsx +417 -0
- package/client/src/components/SquareNode.tsx +797 -0
- package/client/src/components/StartNode.tsx +250 -0
- package/client/src/components/ToolkitNode.tsx +365 -0
- package/client/src/components/TriggerNode.tsx +463 -0
- package/client/src/components/auth/LoginPage.tsx +247 -0
- package/client/src/components/auth/ProtectedRoute.tsx +59 -0
- package/client/src/components/base/BaseChatModelNode.tsx +271 -0
- package/client/src/components/icons/AIProviderIcons.tsx +50 -0
- package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
- package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
- package/client/src/components/maps/index.ts +26 -0
- package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
- package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
- package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
- package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
- package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
- package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
- package/client/src/components/parameterPanel/index.ts +42 -0
- package/client/src/components/shared/DataPanel.tsx +142 -0
- package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
- package/client/src/components/ui/AIResultModal.tsx +204 -0
- package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
- package/client/src/components/ui/CodeEditor.tsx +81 -0
- package/client/src/components/ui/CollapsibleSection.tsx +88 -0
- package/client/src/components/ui/ComponentItem.tsx +154 -0
- package/client/src/components/ui/ComponentPalette.tsx +321 -0
- package/client/src/components/ui/ConsolePanel.tsx +1074 -0
- package/client/src/components/ui/ErrorBoundary.tsx +196 -0
- package/client/src/components/ui/InputNodesPanel.tsx +204 -0
- package/client/src/components/ui/MapSelector.tsx +314 -0
- package/client/src/components/ui/Modal.tsx +149 -0
- package/client/src/components/ui/NodeContextMenu.tsx +192 -0
- package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
- package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
- package/client/src/components/ui/SettingsPanel.tsx +243 -0
- package/client/src/components/ui/TopToolbar.tsx +736 -0
- package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
- package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
- package/client/src/config/antdTheme.ts +186 -0
- package/client/src/config/api.ts +54 -0
- package/client/src/contexts/AuthContext.tsx +221 -0
- package/client/src/contexts/ThemeContext.tsx +42 -0
- package/client/src/contexts/WebSocketContext.tsx +1971 -0
- package/client/src/factories/baseChatModelFactory.ts +256 -0
- package/client/src/hooks/useAndroidOperations.ts +164 -0
- package/client/src/hooks/useApiKeyValidation.ts +107 -0
- package/client/src/hooks/useApiKeys.ts +238 -0
- package/client/src/hooks/useAppTheme.ts +17 -0
- package/client/src/hooks/useComponentPalette.ts +51 -0
- package/client/src/hooks/useCopyPaste.ts +155 -0
- package/client/src/hooks/useDragAndDrop.ts +124 -0
- package/client/src/hooks/useDragVariable.ts +88 -0
- package/client/src/hooks/useExecution.ts +313 -0
- package/client/src/hooks/useParameterPanel.ts +176 -0
- package/client/src/hooks/useReactFlowNodes.ts +189 -0
- package/client/src/hooks/useToolSchema.ts +209 -0
- package/client/src/hooks/useWhatsApp.ts +196 -0
- package/client/src/hooks/useWorkflowManagement.ts +46 -0
- package/client/src/index.css +315 -0
- package/client/src/main.tsx +19 -0
- package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
- package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
- package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
- package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
- package/client/src/nodeDefinitions/chatNodes.ts +135 -0
- package/client/src/nodeDefinitions/codeNodes.ts +54 -0
- package/client/src/nodeDefinitions/documentNodes.ts +379 -0
- package/client/src/nodeDefinitions/index.ts +15 -0
- package/client/src/nodeDefinitions/locationNodes.ts +463 -0
- package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
- package/client/src/nodeDefinitions/skillNodes.ts +211 -0
- package/client/src/nodeDefinitions/toolNodes.ts +198 -0
- package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
- package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
- package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
- package/client/src/nodeDefinitions.ts +104 -0
- package/client/src/schemas/workflowSchema.ts +264 -0
- package/client/src/services/dynamicParameterService.ts +96 -0
- package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
- package/client/src/services/executionService.ts +232 -0
- package/client/src/services/workflowApi.ts +91 -0
- package/client/src/store/useAppStore.ts +582 -0
- package/client/src/styles/theme.ts +508 -0
- package/client/src/styles/zIndex.ts +17 -0
- package/client/src/types/ComponentTypes.ts +39 -0
- package/client/src/types/EdgeCondition.ts +231 -0
- package/client/src/types/INodeProperties.ts +288 -0
- package/client/src/types/NodeTypes.ts +28 -0
- package/client/src/utils/formatters.ts +33 -0
- package/client/src/utils/googleMapsLoader.ts +140 -0
- package/client/src/utils/locationUtils.ts +85 -0
- package/client/src/utils/nodeUtils.ts +31 -0
- package/client/src/utils/workflow.ts +30 -0
- package/client/src/utils/workflowExport.ts +120 -0
- package/client/src/vite-env.d.ts +12 -0
- package/client/tailwind.config.js +60 -0
- package/client/tsconfig.json +25 -0
- package/client/tsconfig.node.json +11 -0
- package/client/vite.config.js +35 -0
- package/docker-compose.prod.yml +107 -0
- package/docker-compose.yml +104 -0
- package/docs-MachinaOs/README.md +85 -0
- package/docs-MachinaOs/deployment/docker.mdx +228 -0
- package/docs-MachinaOs/deployment/production.mdx +345 -0
- package/docs-MachinaOs/docs.json +75 -0
- package/docs-MachinaOs/faq.mdx +309 -0
- package/docs-MachinaOs/favicon.svg +5 -0
- package/docs-MachinaOs/installation.mdx +160 -0
- package/docs-MachinaOs/introduction.mdx +114 -0
- package/docs-MachinaOs/logo/dark.svg +6 -0
- package/docs-MachinaOs/logo/light.svg +6 -0
- package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
- package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
- package/docs-MachinaOs/nodes/android.mdx +411 -0
- package/docs-MachinaOs/nodes/overview.mdx +181 -0
- package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
- package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
- package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
- package/docs-MachinaOs/quickstart.mdx +119 -0
- package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
- package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
- package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
- package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
- package/nul +0 -0
- package/package.json +70 -0
- package/scripts/build.js +158 -0
- package/scripts/check-ports.ps1 +33 -0
- package/scripts/clean.js +40 -0
- package/scripts/docker.js +93 -0
- package/scripts/kill-port.ps1 +154 -0
- package/scripts/start.js +210 -0
- package/scripts/stop.js +325 -0
- package/server/.dockerignore +44 -0
- package/server/Dockerfile +45 -0
- package/server/constants.py +249 -0
- package/server/core/__init__.py +1 -0
- package/server/core/cache.py +461 -0
- package/server/core/config.py +128 -0
- package/server/core/container.py +99 -0
- package/server/core/database.py +1211 -0
- package/server/core/logging.py +314 -0
- package/server/main.py +289 -0
- package/server/middleware/__init__.py +5 -0
- package/server/middleware/auth.py +89 -0
- package/server/models/__init__.py +1 -0
- package/server/models/auth.py +52 -0
- package/server/models/cache.py +24 -0
- package/server/models/database.py +211 -0
- package/server/models/nodes.py +455 -0
- package/server/package.json +9 -0
- package/server/pyproject.toml +72 -0
- package/server/requirements.txt +83 -0
- package/server/routers/__init__.py +1 -0
- package/server/routers/android.py +294 -0
- package/server/routers/auth.py +203 -0
- package/server/routers/database.py +151 -0
- package/server/routers/maps.py +142 -0
- package/server/routers/nodejs_compat.py +289 -0
- package/server/routers/webhook.py +90 -0
- package/server/routers/websocket.py +2127 -0
- package/server/routers/whatsapp.py +761 -0
- package/server/routers/workflow.py +200 -0
- package/server/services/__init__.py +1 -0
- package/server/services/ai.py +2415 -0
- package/server/services/android/__init__.py +27 -0
- package/server/services/android/broadcaster.py +114 -0
- package/server/services/android/client.py +608 -0
- package/server/services/android/manager.py +78 -0
- package/server/services/android/protocol.py +165 -0
- package/server/services/android_service.py +588 -0
- package/server/services/auth.py +131 -0
- package/server/services/chat_client.py +160 -0
- package/server/services/deployment/__init__.py +12 -0
- package/server/services/deployment/manager.py +706 -0
- package/server/services/deployment/state.py +47 -0
- package/server/services/deployment/triggers.py +275 -0
- package/server/services/event_waiter.py +785 -0
- package/server/services/execution/__init__.py +77 -0
- package/server/services/execution/cache.py +769 -0
- package/server/services/execution/conditions.py +373 -0
- package/server/services/execution/dlq.py +132 -0
- package/server/services/execution/executor.py +1351 -0
- package/server/services/execution/models.py +531 -0
- package/server/services/execution/recovery.py +235 -0
- package/server/services/handlers/__init__.py +126 -0
- package/server/services/handlers/ai.py +355 -0
- package/server/services/handlers/android.py +260 -0
- package/server/services/handlers/code.py +278 -0
- package/server/services/handlers/document.py +598 -0
- package/server/services/handlers/http.py +193 -0
- package/server/services/handlers/polyglot.py +105 -0
- package/server/services/handlers/tools.py +845 -0
- package/server/services/handlers/triggers.py +107 -0
- package/server/services/handlers/utility.py +822 -0
- package/server/services/handlers/whatsapp.py +476 -0
- package/server/services/maps.py +289 -0
- package/server/services/memory_store.py +103 -0
- package/server/services/node_executor.py +375 -0
- package/server/services/parameter_resolver.py +218 -0
- package/server/services/polyglot_client.py +169 -0
- package/server/services/scheduler.py +155 -0
- package/server/services/skill_loader.py +417 -0
- package/server/services/status_broadcaster.py +826 -0
- package/server/services/temporal/__init__.py +23 -0
- package/server/services/temporal/activities.py +344 -0
- package/server/services/temporal/client.py +76 -0
- package/server/services/temporal/executor.py +147 -0
- package/server/services/temporal/worker.py +251 -0
- package/server/services/temporal/workflow.py +355 -0
- package/server/services/temporal/ws_client.py +236 -0
- package/server/services/text.py +111 -0
- package/server/services/user_auth.py +172 -0
- package/server/services/websocket_client.py +29 -0
- package/server/services/workflow.py +597 -0
- package/server/skills/android-skill/SKILL.md +82 -0
- package/server/skills/assistant-personality/SKILL.md +45 -0
- package/server/skills/code-skill/SKILL.md +140 -0
- package/server/skills/http-skill/SKILL.md +161 -0
- package/server/skills/maps-skill/SKILL.md +170 -0
- package/server/skills/memory-skill/SKILL.md +154 -0
- package/server/skills/scheduler-skill/SKILL.md +84 -0
- package/server/skills/whatsapp-skill/SKILL.md +283 -0
- package/server/uv.lock +2916 -0
- package/server/whatsapp-rpc/.dockerignore +30 -0
- package/server/whatsapp-rpc/Dockerfile +44 -0
- package/server/whatsapp-rpc/Dockerfile.web +17 -0
- package/server/whatsapp-rpc/README.md +139 -0
- package/server/whatsapp-rpc/cli.js +95 -0
- package/server/whatsapp-rpc/configs/config.yaml +7 -0
- package/server/whatsapp-rpc/docker-compose.yml +35 -0
- package/server/whatsapp-rpc/docs/API.md +410 -0
- package/server/whatsapp-rpc/go.mod +67 -0
- package/server/whatsapp-rpc/go.sum +203 -0
- package/server/whatsapp-rpc/package.json +30 -0
- package/server/whatsapp-rpc/schema.json +1294 -0
- package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
- package/server/whatsapp-rpc/scripts/cli.js +162 -0
- package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
- package/server/whatsapp-rpc/src/go/config/config.go +49 -0
- package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
- package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
- package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
- package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
- package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
- package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
- package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
- package/server/whatsapp-rpc/web/app.py +609 -0
- package/server/whatsapp-rpc/web/requirements.txt +6 -0
- package/server/whatsapp-rpc/web/rpc_client.py +427 -0
- package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
- package/server/whatsapp-rpc/web/templates/base.html +150 -0
- package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
- package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
- package/server/whatsapp-rpc/web/templates/groups.html +328 -0
- package/server/whatsapp-rpc/web/templates/messages.html +465 -0
- package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
- package/server/whatsapp-rpc/web/templates/send.html +259 -0
- 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
|