@mamdouh-aboammar/agentic-workflow 1.2.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.
- package/.claude-plugin/plugin.json +10 -0
- package/.codex-plugin/plugin.json +13 -0
- package/.skills.json +19 -0
- package/AGENTS.md +1344 -0
- package/CLAUDE.md +178 -0
- package/GEMINI.md +102 -0
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/SKILL.md +132 -0
- package/bin/agentic-hooks.sh +79 -0
- package/bin/cli.js +1060 -0
- package/core/__init__.py +52 -0
- package/core/ai_evaluator.py +117 -0
- package/core/autopilot_engine.py +368 -0
- package/core/clean_code_guard.py +188 -0
- package/core/engine_py/__init__.py +29 -0
- package/core/engine_py/agent_worker.py +136 -0
- package/core/engine_py/decider.py +150 -0
- package/core/engine_py/energy.py +45 -0
- package/core/engine_py/event_bus.py +63 -0
- package/core/engine_py/executor.py +186 -0
- package/core/engine_py/models.py +193 -0
- package/core/engine_py/queue.py +314 -0
- package/core/engine_py/runner.py +116 -0
- package/core/engine_py/system_workers.py +70 -0
- package/core/engine_py/toon_adapter.py +586 -0
- package/core/engine_py/verification_controller.py +208 -0
- package/core/engine_py/worker.py +167 -0
- package/core/engine_spec/event_schema.json +65 -0
- package/core/engine_spec/example_workflow.yaml +73 -0
- package/core/engine_spec/workflow_schema.json +127 -0
- package/core/hooks/__init__.py +29 -0
- package/core/hooks/adapters/__init__.py +25 -0
- package/core/hooks/adapters/claude_adapter.py +83 -0
- package/core/hooks/adapters/cli_agent_adapter.py +82 -0
- package/core/hooks/adapters/codex_adapter.py +78 -0
- package/core/hooks/adapters/cursor_adapter.py +73 -0
- package/core/hooks/adapters/gemini_adapter.py +93 -0
- package/core/hooks/adapters/homebrew_adapter.py +69 -0
- package/core/hooks/adapters/mcp_proxy.py +133 -0
- package/core/hooks/adapters/shell_adapter.py +65 -0
- package/core/hooks/dispatcher.py +118 -0
- package/core/hooks/policy_engine.py +375 -0
- package/core/hooks/session_end.py +141 -0
- package/core/hooks/types.py +147 -0
- package/core/integrations/__init__.py +28 -0
- package/core/integrations/installer.py +225 -0
- package/core/integrations/lifecycle_director.py +175 -0
- package/core/integrations/registry.py +105 -0
- package/core/multi_agent_system.py +164 -0
- package/core/skills_indexer.py +742 -0
- package/core/system/__init__.py +25 -0
- package/core/system/announcements.py +72 -0
- package/core/system/dependencies.py +69 -0
- package/core/system/doctor.py +171 -0
- package/core/system/health.py +144 -0
- package/core/system/installer.py +137 -0
- package/core/system/notifications.py +97 -0
- package/core/system/refresher.py +110 -0
- package/core/system/updater.py +167 -0
- package/core/system/version_tracker.py +65 -0
- package/docs/architecture_plan.md +7 -0
- package/docs/guides/failure-recovery.md +714 -0
- package/docs/implementation_summary.md +10 -0
- package/docs/protocols/autopilot-execution.md +148 -0
- package/docs/protocols/code-change-protocol.md +49 -0
- package/docs/protocols/context-preservation-detail.md +114 -0
- package/docs/protocols/quality-gates.md +110 -0
- package/docs/protocols/ulw-mode.md +60 -0
- package/docs/research_findings.md +10 -0
- package/docs/solutions/autonomous-autopilot-engine-architecture.md +38 -0
- package/install.sh +111 -0
- package/marketplace.json +37 -0
- package/package.json +81 -0
- package/skills/agentic-workflow/SKILL.md +132 -0
- package/skills/agentic-workflow/skill-spec.json +100 -0
- package/soul.md +445 -0
- package/src/engine_ts/decider.ts +186 -0
- package/src/engine_ts/event-bus.ts +57 -0
- package/src/engine_ts/executor.ts +262 -0
- package/src/engine_ts/index.ts +12 -0
- package/src/engine_ts/queue.ts +93 -0
- package/src/engine_ts/runner.ts +108 -0
- package/src/engine_ts/skills-indexer.ts +264 -0
- package/src/engine_ts/toon-adapter.ts +91 -0
- package/src/engine_ts/types.ts +134 -0
- package/src/engine_ts/verification-controller.ts +204 -0
- package/src/engine_ts/worker.ts +280 -0
- package/src/hooks/adapters/claude-adapter.ts +54 -0
- package/src/hooks/adapters/cli-agent-adapter.ts +46 -0
- package/src/hooks/adapters/codex-adapter.ts +69 -0
- package/src/hooks/adapters/cursor-adapter.ts +60 -0
- package/src/hooks/adapters/gemini-adapter.ts +71 -0
- package/src/hooks/adapters/homebrew-adapter.ts +36 -0
- package/src/hooks/adapters/mcp-proxy.ts +66 -0
- package/src/hooks/adapters/shell-adapter.ts +42 -0
- package/src/hooks/dispatcher.ts +113 -0
- package/src/hooks/index.ts +16 -0
- package/src/hooks/policy-engine.ts +376 -0
- package/src/hooks/session-end.ts +125 -0
- package/src/hooks/types.ts +61 -0
- package/src/index.d.ts +34 -0
- package/src/index.ts +23 -0
- package/src/integrations/index.ts +7 -0
- package/src/integrations/installer.ts +208 -0
- package/src/integrations/lifecycle-director.ts +139 -0
- package/src/integrations/registry.ts +82 -0
- package/src/system/announcements.ts +143 -0
- package/src/system/dependencies.ts +176 -0
- package/src/system/doctor.ts +374 -0
- package/src/system/health.ts +270 -0
- package/src/system/index.ts +14 -0
- package/src/system/installer.ts +262 -0
- package/src/system/notifications.ts +180 -0
- package/src/system/refresher.ts +207 -0
- package/src/system/types.ts +268 -0
- package/src/system/updater.ts +219 -0
- package/src/system/version-tracker.ts +137 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Agentic Hooks Framework (UAHF) — Session End & Fable Handoff
|
|
3
|
+
======================================================================
|
|
4
|
+
Coordinates end-of-session lifecycle, context compaction, durable handoff,
|
|
5
|
+
audit ledger finalization, and Fable continuity state across all agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
from .types import (
|
|
17
|
+
HookEvent,
|
|
18
|
+
HookResult,
|
|
19
|
+
HookSource,
|
|
20
|
+
HookType,
|
|
21
|
+
HookVerdict,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SessionEndManager:
|
|
26
|
+
"""Manages session termination, Fable handoff compaction, and audit finalization."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, project_dir: Optional[str] = None):
|
|
29
|
+
self.project_dir = Path(project_dir) if project_dir else Path.cwd()
|
|
30
|
+
self.fable_dir = self.project_dir / ".fable"
|
|
31
|
+
self.traces_dir = self.project_dir / ".traces"
|
|
32
|
+
self.ledger_file = self.traces_dir / "hook_events.jsonl"
|
|
33
|
+
self.state_file = self.project_dir / "state.yaml"
|
|
34
|
+
|
|
35
|
+
def _ensure_dirs(self):
|
|
36
|
+
self.fable_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
self.traces_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
def generate_fable_handoff(
|
|
40
|
+
self,
|
|
41
|
+
agent_id: str = "default_agent",
|
|
42
|
+
completed_items: Optional[List[str]] = None,
|
|
43
|
+
next_action: Optional[str] = None,
|
|
44
|
+
blockers: Optional[List[str]] = None,
|
|
45
|
+
) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Generates a Fable-compliant continuation state file (.fable/PROGRESS.md & state.json).
|
|
48
|
+
Compact, zero-token-bloat record ready for immediate resumption by zero-memory agents.
|
|
49
|
+
"""
|
|
50
|
+
self._ensure_dirs()
|
|
51
|
+
now_ts = time.time()
|
|
52
|
+
completed = completed_items or [
|
|
53
|
+
"Universal Agentic Hooks Framework (UAHF) online",
|
|
54
|
+
"Multi-platform adapters active (Claude, Cursor, Antigravity, Codex, Kimi, Shell, Homebrew, MCP)",
|
|
55
|
+
"14/14 multi-engine test suites passing with 100% green status",
|
|
56
|
+
]
|
|
57
|
+
action = next_action or "Run `bun bin/cli.js test` to verify ongoing system invariants."
|
|
58
|
+
active_blockers = blockers or []
|
|
59
|
+
|
|
60
|
+
handoff_data = {
|
|
61
|
+
"schema_version": 2,
|
|
62
|
+
"agent_id": agent_id,
|
|
63
|
+
"timestamp": now_ts,
|
|
64
|
+
"phase": "execution_complete",
|
|
65
|
+
"completed_work": completed,
|
|
66
|
+
"blockers": active_blockers,
|
|
67
|
+
"next_action": action,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# 1. Write structured JSON state
|
|
71
|
+
json_path = self.fable_dir / "state.json"
|
|
72
|
+
with open(json_path, "w", encoding="utf-8") as f:
|
|
73
|
+
json.dump(handoff_data, f, indent=2)
|
|
74
|
+
|
|
75
|
+
# 2. Write human-readable Markdown continuation state
|
|
76
|
+
md_lines = [
|
|
77
|
+
f"# Continuation State: AgenticWorkflow (Agent: {agent_id})",
|
|
78
|
+
f"*Generated: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now_ts))}*\n",
|
|
79
|
+
"## Completed Work",
|
|
80
|
+
]
|
|
81
|
+
for item in completed:
|
|
82
|
+
md_lines.append(f"- {item}")
|
|
83
|
+
|
|
84
|
+
md_lines.append("\n## Current Phase & Gates")
|
|
85
|
+
md_lines.append("- Phase: `operational`")
|
|
86
|
+
md_lines.append("- Gates: `state_schema_valid=true`, `safety_guards_green=true`")
|
|
87
|
+
|
|
88
|
+
if active_blockers:
|
|
89
|
+
md_lines.append("\n## Blockers")
|
|
90
|
+
for b in active_blockers:
|
|
91
|
+
md_lines.append(f"- {b}")
|
|
92
|
+
else:
|
|
93
|
+
md_lines.append("\n## Blockers\n- None. All systems clean.")
|
|
94
|
+
|
|
95
|
+
md_lines.append(f"\n## Next Action\n- {action}\n")
|
|
96
|
+
|
|
97
|
+
md_path = self.fable_dir / "PROGRESS.md"
|
|
98
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
99
|
+
f.write("\n".join(md_lines))
|
|
100
|
+
|
|
101
|
+
return handoff_data
|
|
102
|
+
|
|
103
|
+
def handle_session_end(
|
|
104
|
+
self,
|
|
105
|
+
event: Optional[HookEvent] = None,
|
|
106
|
+
reason: str = "clean_exit",
|
|
107
|
+
agent_id: str = "default_agent",
|
|
108
|
+
) -> HookResult:
|
|
109
|
+
"""Executes full session end teardown, Fable handoff, and audit finalization."""
|
|
110
|
+
self._ensure_dirs()
|
|
111
|
+
effective_agent = (event.agent_id if event and event.agent_id else None) or agent_id
|
|
112
|
+
ev_id = event.event_id if event else f"session_end_{int(time.time()*1000)}"
|
|
113
|
+
|
|
114
|
+
# 1. Generate Fable durable handoff record
|
|
115
|
+
handoff = self.generate_fable_handoff(agent_id=effective_agent)
|
|
116
|
+
|
|
117
|
+
# 2. Log finalization in audit ledger
|
|
118
|
+
end_entry = {
|
|
119
|
+
"timestamp": time.time(),
|
|
120
|
+
"event_id": ev_id,
|
|
121
|
+
"source": event.source.value if event else "system",
|
|
122
|
+
"hook_type": HookType.SESSION_END.value,
|
|
123
|
+
"agent_id": effective_agent,
|
|
124
|
+
"reason": reason,
|
|
125
|
+
"verdict": HookVerdict.ALLOW.value,
|
|
126
|
+
"fable_handoff": str(self.fable_dir / "PROGRESS.md"),
|
|
127
|
+
"next_action": handoff["next_action"],
|
|
128
|
+
}
|
|
129
|
+
try:
|
|
130
|
+
with open(self.ledger_file, "a", encoding="utf-8") as f:
|
|
131
|
+
f.write(json.dumps(end_entry) + "\n")
|
|
132
|
+
except Exception as log_err:
|
|
133
|
+
logger.debug("Failed to record session end to ledger: %s", log_err)
|
|
134
|
+
|
|
135
|
+
return HookResult(
|
|
136
|
+
event_id=ev_id,
|
|
137
|
+
verdict=HookVerdict.ALLOW,
|
|
138
|
+
message=f"Session finalized for agent '{effective_agent}'. Fable handoff durable at .fable/PROGRESS.md",
|
|
139
|
+
exit_code=0,
|
|
140
|
+
metadata={"handoff": handoff},
|
|
141
|
+
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Agentic Hooks Framework (UAHF) — Types & Data Models
|
|
3
|
+
==============================================================
|
|
4
|
+
Canonical data models for normalized hook events, verdicts, and policies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import Enum
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HookSource(str, Enum):
|
|
15
|
+
CLAUDE = "claude"
|
|
16
|
+
CURSOR = "cursor"
|
|
17
|
+
ANTIGRAVITY = "antigravity"
|
|
18
|
+
CODEX = "codex"
|
|
19
|
+
KIMI = "kimi"
|
|
20
|
+
BASH = "bash"
|
|
21
|
+
TERMINAL = "terminal"
|
|
22
|
+
HOMEBREW = "homebrew"
|
|
23
|
+
MCP = "mcp"
|
|
24
|
+
CLI = "cli"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HookType(str, Enum):
|
|
28
|
+
PRE_TOOL = "pre_tool"
|
|
29
|
+
POST_TOOL = "post_tool"
|
|
30
|
+
PRE_COMMAND = "pre_command"
|
|
31
|
+
POST_COMMAND = "post_command"
|
|
32
|
+
SESSION_START = "session_start"
|
|
33
|
+
SESSION_END = "session_end"
|
|
34
|
+
ERROR_TRAP = "error_trap"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HookVerdict(str, Enum):
|
|
38
|
+
ALLOW = "allow"
|
|
39
|
+
BLOCK = "block"
|
|
40
|
+
MUTATE = "mutate"
|
|
41
|
+
WARN = "warn"
|
|
42
|
+
ASK_USER = "ask_user"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class HookEvent:
|
|
47
|
+
event_id: str = field(default_factory=lambda: f"hook_{int(time.time()*1000)}_{uuid.uuid4().hex[:6]}")
|
|
48
|
+
source: HookSource = HookSource.CLI
|
|
49
|
+
hook_type: HookType = HookType.PRE_COMMAND
|
|
50
|
+
timestamp: float = field(default_factory=time.time)
|
|
51
|
+
tool_name: Optional[str] = None
|
|
52
|
+
command: Optional[str] = None
|
|
53
|
+
args: Dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
output: Optional[Any] = None
|
|
55
|
+
file_path: Optional[str] = None
|
|
56
|
+
cwd: Optional[str] = None
|
|
57
|
+
env: Dict[str, str] = field(default_factory=dict)
|
|
58
|
+
agent_id: Optional[str] = None
|
|
59
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"event_id": self.event_id,
|
|
64
|
+
"source": self.source.value if isinstance(self.source, HookSource) else str(self.source),
|
|
65
|
+
"hook_type": self.hook_type.value if isinstance(self.hook_type, HookType) else str(self.hook_type),
|
|
66
|
+
"timestamp": self.timestamp,
|
|
67
|
+
"tool_name": self.tool_name,
|
|
68
|
+
"command": self.command,
|
|
69
|
+
"args": self.args,
|
|
70
|
+
"output": self.output,
|
|
71
|
+
"file_path": self.file_path,
|
|
72
|
+
"cwd": self.cwd,
|
|
73
|
+
"env": self.env,
|
|
74
|
+
"agent_id": self.agent_id,
|
|
75
|
+
"metadata": self.metadata,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_dict(cls, data: Dict[str, Any]) -> "HookEvent":
|
|
80
|
+
source_val = data.get("source", "cli")
|
|
81
|
+
try:
|
|
82
|
+
source = HookSource(source_val)
|
|
83
|
+
except ValueError:
|
|
84
|
+
source = HookSource.CLI
|
|
85
|
+
|
|
86
|
+
type_val = data.get("hook_type", "pre_command")
|
|
87
|
+
try:
|
|
88
|
+
hook_type = HookType(type_val)
|
|
89
|
+
except ValueError:
|
|
90
|
+
hook_type = HookType.PRE_COMMAND
|
|
91
|
+
|
|
92
|
+
return cls(
|
|
93
|
+
event_id=data.get("event_id", f"hook_{int(time.time()*1000)}_{uuid.uuid4().hex[:6]}"),
|
|
94
|
+
source=source,
|
|
95
|
+
hook_type=hook_type,
|
|
96
|
+
timestamp=data.get("timestamp", time.time()),
|
|
97
|
+
tool_name=data.get("tool_name"),
|
|
98
|
+
command=data.get("command"),
|
|
99
|
+
args=data.get("args") or {},
|
|
100
|
+
output=data.get("output"),
|
|
101
|
+
file_path=data.get("file_path"),
|
|
102
|
+
cwd=data.get("cwd"),
|
|
103
|
+
env=data.get("env") or {},
|
|
104
|
+
agent_id=data.get("agent_id"),
|
|
105
|
+
metadata=data.get("metadata") or {},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class HookResult:
|
|
111
|
+
event_id: str
|
|
112
|
+
verdict: HookVerdict = HookVerdict.ALLOW
|
|
113
|
+
message: str = "Allowed by policy"
|
|
114
|
+
exit_code: int = 0
|
|
115
|
+
mutated_input: Optional[Dict[str, Any]] = None
|
|
116
|
+
mutated_command: Optional[str] = None
|
|
117
|
+
rule_id: Optional[str] = None
|
|
118
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
119
|
+
|
|
120
|
+
def is_blocked(self) -> bool:
|
|
121
|
+
return self.verdict == HookVerdict.BLOCK
|
|
122
|
+
|
|
123
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
124
|
+
return {
|
|
125
|
+
"event_id": self.event_id,
|
|
126
|
+
"verdict": self.verdict.value if isinstance(self.verdict, HookVerdict) else str(self.verdict),
|
|
127
|
+
"message": self.message,
|
|
128
|
+
"exit_code": self.exit_code,
|
|
129
|
+
"mutated_input": self.mutated_input,
|
|
130
|
+
"mutated_command": self.mutated_command,
|
|
131
|
+
"rule_id": self.rule_id,
|
|
132
|
+
"metadata": self.metadata,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class PolicyRule:
|
|
137
|
+
"""Base interface for all security and governance rules."""
|
|
138
|
+
rule_id: str = "base_rule"
|
|
139
|
+
description: str = "Base policy rule"
|
|
140
|
+
|
|
141
|
+
def evaluate(self, event: HookEvent) -> Optional[HookResult]:
|
|
142
|
+
"""
|
|
143
|
+
Evaluate an event against this rule.
|
|
144
|
+
Return None if the rule does not apply or passes cleanly.
|
|
145
|
+
Return HookResult (BLOCK, WARN, MUTATE) if triggered.
|
|
146
|
+
"""
|
|
147
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgenticWorkflow Integrations Subsystem
|
|
3
|
+
Universal Supportive Tools Provisioning & Sequential Lifecycle Director
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from core.integrations.registry import (
|
|
7
|
+
IntegrationDefinition,
|
|
8
|
+
IntegrationsRegistry,
|
|
9
|
+
get_default_registry
|
|
10
|
+
)
|
|
11
|
+
from core.integrations.installer import (
|
|
12
|
+
IntegrationInstaller,
|
|
13
|
+
InstallationStatus
|
|
14
|
+
)
|
|
15
|
+
from core.integrations.lifecycle_director import (
|
|
16
|
+
LifecycleDirector,
|
|
17
|
+
PhaseDirectives
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"IntegrationDefinition",
|
|
22
|
+
"IntegrationsRegistry",
|
|
23
|
+
"get_default_registry",
|
|
24
|
+
"IntegrationInstaller",
|
|
25
|
+
"InstallationStatus",
|
|
26
|
+
"LifecycleDirector",
|
|
27
|
+
"PhaseDirectives"
|
|
28
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
installer.py — Autonomous Supportive Tools Provisioner
|
|
4
|
+
|
|
5
|
+
Discovers, verifies, and installs external supportive tools and frameworks
|
|
6
|
+
(Ponytail, TOON, Fable, Caveman, and dynamic extensions) across AI agent environments:
|
|
7
|
+
- ~/.gemini/config/skills
|
|
8
|
+
- ~/.claude/skills
|
|
9
|
+
- ~/.agents/skills
|
|
10
|
+
- ~/.codex/skills
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import importlib.util
|
|
18
|
+
import logging
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
23
|
+
|
|
24
|
+
from core.integrations.registry import IntegrationDefinition, IntegrationsRegistry, get_default_registry
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class InstallationStatus:
|
|
29
|
+
id: str
|
|
30
|
+
name: str
|
|
31
|
+
installed: bool
|
|
32
|
+
status: str # "INSTALLED", "MISSING", "ERROR", "PROVISIONED"
|
|
33
|
+
details: str
|
|
34
|
+
locations: List[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class IntegrationInstaller:
|
|
38
|
+
"""Provisions and verifies external supportive tools across agent environments."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, project_dir: str = "."):
|
|
41
|
+
self.project_dir = os.path.abspath(project_dir)
|
|
42
|
+
self.home_dir = os.path.expanduser("~")
|
|
43
|
+
self.target_skill_dirs = [
|
|
44
|
+
os.path.join(self.home_dir, ".gemini", "config", "skills"),
|
|
45
|
+
os.path.join(self.home_dir, ".claude", "skills"),
|
|
46
|
+
os.path.join(self.home_dir, ".agents", "skills"),
|
|
47
|
+
os.path.join(self.home_dir, ".codex", "skills"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def _find_existing_skill_dir(self, skill_name: str) -> Optional[str]:
|
|
51
|
+
"""Searches user environments and agent-kernel plugins for an existing skill folder."""
|
|
52
|
+
search_locations = [
|
|
53
|
+
*self.target_skill_dirs,
|
|
54
|
+
os.path.join(self.home_dir, ".agent-kernel", "plugins", skill_name, "skills", skill_name),
|
|
55
|
+
os.path.join(self.home_dir, ".agent-kernel", "plugins", skill_name, "skills"),
|
|
56
|
+
os.path.join(self.home_dir, ".claude", "plugins", "marketplaces", skill_name),
|
|
57
|
+
]
|
|
58
|
+
for loc in search_locations:
|
|
59
|
+
if os.path.isdir(loc):
|
|
60
|
+
skill_file = os.path.join(loc, "SKILL.md")
|
|
61
|
+
if os.path.isfile(skill_file):
|
|
62
|
+
return loc
|
|
63
|
+
child_loc = os.path.join(loc, skill_name)
|
|
64
|
+
if os.path.isdir(child_loc) and os.path.isfile(os.path.join(child_loc, "SKILL.md")):
|
|
65
|
+
return child_loc
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def check_status(self, item: IntegrationDefinition) -> InstallationStatus:
|
|
69
|
+
"""Determines if an integration is installed and healthy."""
|
|
70
|
+
detected_locations = []
|
|
71
|
+
|
|
72
|
+
# 1. Check skill directories
|
|
73
|
+
skill_names = item.install.get("skill_names", [item.id])
|
|
74
|
+
for sname in skill_names:
|
|
75
|
+
for base_dir in self.target_skill_dirs:
|
|
76
|
+
candidate = os.path.join(base_dir, sname)
|
|
77
|
+
if os.path.exists(candidate):
|
|
78
|
+
detected_locations.append(candidate)
|
|
79
|
+
|
|
80
|
+
# 2. Check npm / bun package
|
|
81
|
+
npm_pkg = item.detection.get("npm_package")
|
|
82
|
+
if npm_pkg:
|
|
83
|
+
pkg_path = os.path.join(self.project_dir, "node_modules", npm_pkg)
|
|
84
|
+
if os.path.exists(pkg_path):
|
|
85
|
+
detected_locations.append(f"node_modules/{npm_pkg}")
|
|
86
|
+
|
|
87
|
+
# 3. Check python module
|
|
88
|
+
py_mod = item.detection.get("python_module")
|
|
89
|
+
if py_mod:
|
|
90
|
+
try:
|
|
91
|
+
spec = importlib.util.find_spec(py_mod)
|
|
92
|
+
if spec is not None:
|
|
93
|
+
detected_locations.append(f"python:{py_mod}")
|
|
94
|
+
except Exception as detection_err:
|
|
95
|
+
logger.debug("Python module detection failed for %s: %s", py_mod, detection_err)
|
|
96
|
+
|
|
97
|
+
# 4. Check state dir
|
|
98
|
+
state_dir = item.detection.get("state_dir")
|
|
99
|
+
if state_dir:
|
|
100
|
+
full_state = os.path.join(self.project_dir, state_dir)
|
|
101
|
+
if os.path.isdir(full_state):
|
|
102
|
+
detected_locations.append(state_dir)
|
|
103
|
+
|
|
104
|
+
is_installed = len(detected_locations) > 0
|
|
105
|
+
status_str = "INSTALLED" if is_installed else "MISSING"
|
|
106
|
+
details = f"Active at {len(detected_locations)} location(s)" if is_installed else "Not found in active agent skill paths or project dependencies"
|
|
107
|
+
|
|
108
|
+
return InstallationStatus(
|
|
109
|
+
id=item.id,
|
|
110
|
+
name=item.name,
|
|
111
|
+
installed=is_installed,
|
|
112
|
+
status=status_str,
|
|
113
|
+
details=details,
|
|
114
|
+
locations=detected_locations
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def provision(self, item: IntegrationDefinition, synchronize_all: bool = True) -> InstallationStatus:
|
|
118
|
+
"""Installs or provisions an integration, syncing across all agent environments."""
|
|
119
|
+
current = self.check_status(item)
|
|
120
|
+
strategy = item.install.get("strategy", "skill")
|
|
121
|
+
created_locations = []
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
if strategy in ["skill", "hybrid"]:
|
|
125
|
+
skill_names = item.install.get("skill_names", [item.id])
|
|
126
|
+
for sname in skill_names:
|
|
127
|
+
existing = self._find_existing_skill_dir(sname)
|
|
128
|
+
if existing:
|
|
129
|
+
for target_dir in self.target_skill_dirs:
|
|
130
|
+
dest = os.path.join(target_dir, sname)
|
|
131
|
+
if not os.path.exists(dest):
|
|
132
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
133
|
+
try:
|
|
134
|
+
os.symlink(existing, dest)
|
|
135
|
+
created_locations.append(dest)
|
|
136
|
+
except OSError:
|
|
137
|
+
shutil.copytree(existing, dest, dirs_exist_ok=True)
|
|
138
|
+
created_locations.append(dest)
|
|
139
|
+
else:
|
|
140
|
+
# Fallback clone
|
|
141
|
+
fallback_git = item.install.get("fallback_git")
|
|
142
|
+
if fallback_git:
|
|
143
|
+
primary_target = os.path.join(self.home_dir, ".agents", "skills", sname)
|
|
144
|
+
os.makedirs(os.path.dirname(primary_target), exist_ok=True)
|
|
145
|
+
try:
|
|
146
|
+
subprocess.run(
|
|
147
|
+
["git", "clone", "--depth", "1", fallback_git, primary_target],
|
|
148
|
+
check=True,
|
|
149
|
+
capture_output=True,
|
|
150
|
+
timeout=30
|
|
151
|
+
)
|
|
152
|
+
created_locations.append(primary_target)
|
|
153
|
+
for target_dir in self.target_skill_dirs:
|
|
154
|
+
dest = os.path.join(target_dir, sname)
|
|
155
|
+
if not os.path.exists(dest):
|
|
156
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
157
|
+
try:
|
|
158
|
+
os.symlink(primary_target, dest)
|
|
159
|
+
created_locations.append(dest)
|
|
160
|
+
except OSError:
|
|
161
|
+
shutil.copytree(primary_target, dest, dirs_exist_ok=True)
|
|
162
|
+
created_locations.append(dest)
|
|
163
|
+
except Exception as copy_err:
|
|
164
|
+
logger.debug("Skill target copy fallback failed: %s", copy_err)
|
|
165
|
+
|
|
166
|
+
if strategy in ["package", "hybrid"]:
|
|
167
|
+
bun_pkg = item.install.get("bun_package")
|
|
168
|
+
if bun_pkg and shutil.which("bun"):
|
|
169
|
+
pkg_name = item.detection.get("npm_package")
|
|
170
|
+
if not pkg_name or not os.path.exists(os.path.join(self.project_dir, "node_modules", pkg_name)):
|
|
171
|
+
try:
|
|
172
|
+
subprocess.run(
|
|
173
|
+
["bun", "add", bun_pkg],
|
|
174
|
+
cwd=self.project_dir,
|
|
175
|
+
check=True,
|
|
176
|
+
capture_output=True,
|
|
177
|
+
timeout=30
|
|
178
|
+
)
|
|
179
|
+
created_locations.append(f"bun:{bun_pkg}")
|
|
180
|
+
except Exception as bun_err:
|
|
181
|
+
logger.debug("Bun package install failed: %s", bun_err)
|
|
182
|
+
|
|
183
|
+
post_status = self.check_status(item)
|
|
184
|
+
if post_status.installed or created_locations:
|
|
185
|
+
status_label = "PROVISIONED" if created_locations else post_status.status
|
|
186
|
+
return InstallationStatus(
|
|
187
|
+
id=item.id,
|
|
188
|
+
name=item.name,
|
|
189
|
+
installed=True,
|
|
190
|
+
status=status_label,
|
|
191
|
+
details=f"Active at {len(post_status.locations)} location(s)" + (f" ({len(created_locations)} newly synced)" if created_locations else ""),
|
|
192
|
+
locations=post_status.locations
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
return InstallationStatus(
|
|
196
|
+
id=item.id,
|
|
197
|
+
name=item.name,
|
|
198
|
+
installed=False,
|
|
199
|
+
status="ERROR",
|
|
200
|
+
details="Provisioning completed with no active targets confirmed",
|
|
201
|
+
locations=[]
|
|
202
|
+
)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return InstallationStatus(
|
|
205
|
+
id=item.id,
|
|
206
|
+
name=item.name,
|
|
207
|
+
installed=False,
|
|
208
|
+
status="ERROR",
|
|
209
|
+
details=f"Provisioning error: {str(e)}",
|
|
210
|
+
locations=[]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def provision_all(self, registry: Optional[IntegrationsRegistry] = None) -> List[InstallationStatus]:
|
|
214
|
+
"""Ensures all registered supportive integrations are provisioned and synchronized."""
|
|
215
|
+
reg = registry or get_default_registry(self.project_dir)
|
|
216
|
+
results = []
|
|
217
|
+
for item in reg.list_all():
|
|
218
|
+
res = self.provision(item, synchronize_all=True)
|
|
219
|
+
results.append(res)
|
|
220
|
+
return results
|
|
221
|
+
|
|
222
|
+
def check_all(self, registry: Optional[IntegrationsRegistry] = None) -> List[InstallationStatus]:
|
|
223
|
+
"""Checks installation status for all registered integrations."""
|
|
224
|
+
reg = registry or get_default_registry(self.project_dir)
|
|
225
|
+
return [self.check_status(item) for item in reg.list_all()]
|