Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 35 additions & 14 deletions src/appliers/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@
from appliers.manifest import ToolManifest
from frontmatter_parser import render_frontmatter

CLAUDE_DIR = Path.home() / ".claude"
CLAUDE_JSON = Path.home() / ".claude.json"
CLAUDE_COMMANDS_DIR = CLAUDE_DIR / "commands"
CLAUDE_SKILLS_DIR = CLAUDE_DIR / "skills"
CLAUDE_MD = CLAUDE_DIR / "CLAUDE.md"

CLAUDE_MEMORY_SCHEMA = """
Claude Code reads instructions from ~/.claude/CLAUDE.md.
This is a plain Markdown file with no special schema.
Expand All @@ -24,13 +18,40 @@
"""


def _claude_dir() -> Path:
return Path.home() / ".claude"


def _claude_json() -> Path:
return Path.home() / ".claude.json"


def _claude_commands_dir() -> Path:
return Path.home() / ".claude/commands"


def _claude_skills_dir() -> Path:
return Path.home() / ".claude/skills"


def _claude_md() -> Path:
return Path.home() / ".claude/CLAUDE.md"


class ClaudeApplier(BaseApplier):
SKILL_DIR = CLAUDE_SKILLS_DIR
@property
def SKILL_DIR(self):
return getattr(self, "_skill_dir_override", None) or _claude_skills_dir()

@SKILL_DIR.setter
def SKILL_DIR(self, value):
self._skill_dir_override = value

TOOL_NAME = "claude-code"
MEMORY_SCHEMA = CLAUDE_MEMORY_SCHEMA

def apply_skills(self, skills: List[Dict], manifest: ToolManifest) -> int:
CLAUDE_COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
_claude_commands_dir().mkdir(parents=True, exist_ok=True)
count = 0
for skill in skills:
name = skill.get("name", "unnamed")
Expand All @@ -43,7 +64,7 @@ def apply_skills(self, skills: List[Dict], manifest: ToolManifest) -> int:
metadata["version"] = skill["version"]

content = render_frontmatter(metadata, skill.get("body", ""))
path = CLAUDE_COMMANDS_DIR / f"{name}.md"
path = _claude_commands_dir() / f"{name}.md"
path.write_text(content, encoding="utf-8")
manifest.record_skill(name, file_path=str(path), content=content)
count += 1
Expand All @@ -57,9 +78,9 @@ def apply_mcp_servers(
override: bool = False,
) -> int:
# Read existing claude.json or start fresh
if CLAUDE_JSON.exists():
if _claude_json().exists():
try:
data = json.loads(CLAUDE_JSON.read_text(encoding="utf-8"))
data = json.loads(_claude_json().read_text(encoding="utf-8"))
except json.JSONDecodeError:
data = {}
else:
Expand Down Expand Up @@ -100,15 +121,15 @@ def apply_mcp_servers(
count += 1

data["mcpServers"] = mcp_servers
CLAUDE_JSON.write_text(json.dumps(data, indent=2), encoding="utf-8")
_claude_json().write_text(json.dumps(data, indent=2), encoding="utf-8")
return count

def _read_existing_memory_files(self) -> Dict[str, str]:
"""Return {file_path: content} for Claude's memory files."""
result = {}
if CLAUDE_MD.exists():
if _claude_md().exists():
try:
result[str(CLAUDE_MD)] = CLAUDE_MD.read_text(encoding="utf-8")
result[str(_claude_md())] = _claude_md().read_text(encoding="utf-8")
except IOError:
pass
return result
28 changes: 18 additions & 10 deletions src/appliers/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
from appliers.base import BaseApplier
from appliers.manifest import ToolManifest

GEMINI_DIR = Path.home() / ".gemini"
GEMINI_SETTINGS = GEMINI_DIR / "settings.json"
GEMINI_MD = GEMINI_DIR / "GEMINI.md"

GEMINI_MEMORY_SCHEMA = """
Gemini CLI reads instructions from ~/.gemini/GEMINI.md (global) and ./GEMINI.md (per-project).
This applier targets the GLOBAL file at ~/.gemini/GEMINI.md.
Expand Down Expand Up @@ -63,6 +59,18 @@
"""


def _gemini_dir() -> Path:
return Path.home() / ".gemini"


def _gemini_settings() -> Path:
return Path.home() / ".gemini/settings.json"


def _gemini_md() -> Path:
return Path.home() / ".gemini/GEMINI.md"


class GeminiApplier(BaseApplier):
TOOL_NAME = "gemini-cli"
MEMORY_SCHEMA = GEMINI_MEMORY_SCHEMA
Expand All @@ -77,9 +85,9 @@ def apply_mcp_servers(
manifest: ToolManifest,
override: bool = False,
) -> int:
if GEMINI_SETTINGS.exists():
if _gemini_settings().exists():
try:
data = json.loads(GEMINI_SETTINGS.read_text(encoding="utf-8"))
data = json.loads(_gemini_settings().read_text(encoding="utf-8"))
except json.JSONDecodeError:
data = {}
else:
Expand Down Expand Up @@ -119,16 +127,16 @@ def apply_mcp_servers(
count += 1

data["mcpServers"] = mcp_servers
GEMINI_DIR.mkdir(parents=True, exist_ok=True)
GEMINI_SETTINGS.write_text(json.dumps(data, indent=2), encoding="utf-8")
_gemini_dir().mkdir(parents=True, exist_ok=True)
_gemini_settings().write_text(json.dumps(data, indent=2), encoding="utf-8")
return count

def _read_existing_memory_files(self) -> Dict[str, str]:
"""Return {file_path: content} for Gemini's memory files."""
result = {}
if GEMINI_MD.exists():
if _gemini_md().exists():
try:
result[str(GEMINI_MD)] = GEMINI_MD.read_text(encoding="utf-8")
result[str(_gemini_md())] = _gemini_md().read_text(encoding="utf-8")
except IOError:
pass
return result
64 changes: 47 additions & 17 deletions src/appliers/openclaw.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,6 @@
from appliers.manifest import ToolManifest
from frontmatter_parser import render_frontmatter

OPENCLAW_DIR = Path.home() / ".openclaw"
OPENCLAW_SKILLS_DIR = OPENCLAW_DIR / "skills"
OPENCLAW_WORKSPACE = OPENCLAW_DIR / "workspace"
OPENCLAW_USER_MD = OPENCLAW_WORKSPACE / "USER.md"
OPENCLAW_MEMORY_MD = OPENCLAW_WORKSPACE / "MEMORY.md"
OPENCLAW_IDENTITY_MD = OPENCLAW_WORKSPACE / "IDENTITY.md"
OPENCLAW_SOUL_MD = OPENCLAW_WORKSPACE / "SOUL.md"
OPENCLAW_TOOLS_MD = OPENCLAW_WORKSPACE / "TOOLS.md"

OPENCLAW_MEMORY_SCHEMA = """
OpenClaw uses these files in ~/.openclaw/workspace/:
1. USER.md — Personal context about the user (name, timezone, pronouns, preferences).
Expand All @@ -29,13 +20,52 @@
"""


def _openclaw_dir() -> Path:
return Path.home() / ".openclaw"


def _openclaw_skills_dir() -> Path:
return Path.home() / ".openclaw/skills"


def _openclaw_workspace() -> Path:
return Path.home() / ".openclaw/workspace"


def _openclaw_user_md() -> Path:
return Path.home() / ".openclaw/workspace/USER.md"


def _openclaw_memory_md() -> Path:
return Path.home() / ".openclaw/workspace/MEMORY.md"


def _openclaw_identity_md() -> Path:
return Path.home() / ".openclaw/workspace/IDENTITY.md"


def _openclaw_soul_md() -> Path:
return Path.home() / ".openclaw/workspace/SOUL.md"


def _openclaw_tools_md() -> Path:
return Path.home() / ".openclaw/workspace/TOOLS.md"


class OpenClawApplier(BaseApplier):
SKILL_DIR = OPENCLAW_SKILLS_DIR
@property
def SKILL_DIR(self):
return getattr(self, "_skill_dir_override", None) or _openclaw_skills_dir()

@SKILL_DIR.setter
def SKILL_DIR(self, value):
self._skill_dir_override = value

TOOL_NAME = "openclaw"
MEMORY_SCHEMA = OPENCLAW_MEMORY_SCHEMA

def apply_skills(self, skills: List[Dict], manifest: ToolManifest) -> int:
OPENCLAW_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
_openclaw_skills_dir().mkdir(parents=True, exist_ok=True)
count = 0
for skill in skills:
name = skill.get("name", "unnamed")
Expand All @@ -50,7 +80,7 @@ def apply_skills(self, skills: List[Dict], manifest: ToolManifest) -> int:
content = render_frontmatter(metadata, skill.get("body", ""))

# OpenClaw uses directory-based skills: <name>/SKILL.md
skill_dir = OPENCLAW_SKILLS_DIR / name
skill_dir = _openclaw_skills_dir() / name
skill_dir.mkdir(parents=True, exist_ok=True)
path = skill_dir / "SKILL.md"
path.write_text(content, encoding="utf-8")
Expand All @@ -72,11 +102,11 @@ def _read_existing_memory_files(self) -> Dict[str, str]:
"""Return {file_path: content} for OpenClaw's memory files."""
result = {}
for path in [
OPENCLAW_USER_MD,
OPENCLAW_MEMORY_MD,
OPENCLAW_IDENTITY_MD,
OPENCLAW_SOUL_MD,
OPENCLAW_TOOLS_MD,
_openclaw_user_md(),
_openclaw_memory_md(),
_openclaw_identity_md(),
_openclaw_soul_md(),
_openclaw_tools_md(),
]:
if path.exists():
try:
Expand Down
32 changes: 22 additions & 10 deletions src/appliers/windsurf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
from appliers.base import BaseApplier
from appliers.manifest import ToolManifest

WINDSURF_DIR = Path.home() / ".codeium" / "windsurf"
WINDSURF_MCP_CONFIG = WINDSURF_DIR / "mcp_config.json"
WINDSURF_MEMORIES_DIR = WINDSURF_DIR / "memories"
WINDSURF_GLOBAL_RULES = WINDSURF_MEMORIES_DIR / "global_rules.md"
WINDSURF_RULES_DIR = Path(".windsurf") / "rules"

WINDSURF_MEMORY_SCHEMA = """
Expand Down Expand Up @@ -75,6 +71,22 @@
"""


def _windsurf_dir() -> Path:
return Path.home() / ".codeium/windsurf"


def _windsurf_mcp_config() -> Path:
return Path.home() / ".codeium/windsurf/mcp_config.json"


def _windsurf_memories_dir() -> Path:
return Path.home() / ".codeium/windsurf/memories"


def _windsurf_global_rules() -> Path:
return Path.home() / ".codeium/windsurf/memories/global_rules.md"


class WindsurfApplier(BaseApplier):
TOOL_NAME = "windsurf"
MEMORY_SCHEMA = WINDSURF_MEMORY_SCHEMA
Expand All @@ -89,9 +101,9 @@ def apply_mcp_servers(
manifest: ToolManifest,
override: bool = False,
) -> int:
if WINDSURF_MCP_CONFIG.exists():
if _windsurf_mcp_config().exists():
try:
data = json.loads(WINDSURF_MCP_CONFIG.read_text(encoding="utf-8"))
data = json.loads(_windsurf_mcp_config().read_text(encoding="utf-8"))
except json.JSONDecodeError:
data = {}
else:
Expand Down Expand Up @@ -131,17 +143,17 @@ def apply_mcp_servers(
count += 1

data["mcpServers"] = mcp_servers
WINDSURF_DIR.mkdir(parents=True, exist_ok=True)
WINDSURF_MCP_CONFIG.write_text(json.dumps(data, indent=2), encoding="utf-8")
_windsurf_dir().mkdir(parents=True, exist_ok=True)
_windsurf_mcp_config().write_text(json.dumps(data, indent=2), encoding="utf-8")
return count

def _read_existing_memory_files(self) -> Dict[str, str]:
"""Return {file_path: content} for Windsurf's rule/memory files."""
result = {}
# Global rules
if WINDSURF_GLOBAL_RULES.exists():
if _windsurf_global_rules().exists():
try:
result[str(WINDSURF_GLOBAL_RULES)] = WINDSURF_GLOBAL_RULES.read_text(
result[str(_windsurf_global_rules())] = _windsurf_global_rules().read_text(
encoding="utf-8"
)
except IOError:
Expand Down
2 changes: 2 additions & 0 deletions src/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ def tools_status_table(tools: List[Dict[str, str]]) -> None:
st = tool.get("status", "detected")
if st == "synced":
badge = Text("● synced", style="bold green")
elif st == "out of sync":
badge = Text("⚠ out of sync", style="bold yellow")
elif st == "not synced":
badge = Text("○ not synced", style="yellow")
else:
Expand Down
Loading