unlegacy-cli 0.1.0__tar.gz

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.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: unlegacy-cli
3
+ Version: 0.1.0
4
+ Summary: Unlegacy setup CLI for external coding agents
5
+ Author: Unlegacy
6
+ Project-URL: Homepage, https://app.unlegacy.ai
7
+ Project-URL: Repository, https://github.com/unlegacy-ai/unlegacy-core
8
+ Keywords: unlegacy,mcp,cli,coding-agents
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Topic :: Software Development
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0; extra == "dev"
19
+
20
+ # unlegacy-cli
21
+
22
+ Installable CLI for connecting local coding agents to Unlegacy MCP.
23
+
24
+ Unlegacy generates repository documentation and knowledge graph data in the
25
+ platform. This CLI configures external coding agents so they can consume that
26
+ MCP surface.
27
+
28
+ ## Install
29
+
30
+ Published package:
31
+
32
+ ```sh
33
+ uv tool install unlegacy-cli
34
+ ```
35
+
36
+ or:
37
+
38
+ ```sh
39
+ pip install unlegacy-cli
40
+ ```
41
+
42
+ After install, the command is saved on the user's machine as:
43
+
44
+ ```sh
45
+ unlegacy --help
46
+ ```
47
+
48
+ From a checkout:
49
+
50
+ ```sh
51
+ pip install .
52
+ ```
53
+
54
+ or:
55
+
56
+ ```sh
57
+ uv tool install .
58
+ ```
59
+
60
+ One-shot setup without preinstalling:
61
+
62
+ ```sh
63
+ uv tool run --from unlegacy-cli unlegacy setup --token mcp_live_xxx --endpoint https://app.unlegacy.ai/ai/mcp
64
+ ```
65
+
66
+ ## Distribution
67
+
68
+ Build the package artifacts:
69
+
70
+ ```sh
71
+ uv build
72
+ ```
73
+
74
+ The build creates `dist/unlegacy_cli-<version>.tar.gz` and
75
+ `dist/unlegacy_cli-<version>-py3-none-any.whl`. Publish those artifacts to the
76
+ Python package index used by Unlegacy clients.
77
+
78
+ ## Setup
79
+
80
+ ```sh
81
+ unlegacy setup
82
+ ```
83
+
84
+ The flow:
85
+
86
+ 1. Shows the Unlegacy logo.
87
+ 2. Prompts for the MCP token from the Unlegacy frontend.
88
+ 3. Prompts for the MCP endpoint. Press Enter to use the local app endpoint: `http://localhost:5173/ai/mcp`.
89
+ 4. Lets the user select supported clients.
90
+ 5. Writes or merges MCP config.
91
+ 6. Installs the packaged `unlegacy` skill.
92
+ 7. Runs `unlegacy doctor`.
93
+
94
+ Non-interactive usage:
95
+
96
+ ```sh
97
+ UNLEGACY_MCP_TOKEN=mcp_live_xxx \
98
+ unlegacy setup --client claude-code --client codex --endpoint https://app.unlegacy.ai/ai/mcp
99
+ ```
100
+
101
+ For local development:
102
+
103
+ ```sh
104
+ UNLEGACY_MCP_TOKEN=mcp_live_xxx \
105
+ unlegacy setup --client claude-code --client codex --endpoint http://localhost:5173/ai/mcp
106
+ ```
107
+
108
+ Supported client values:
109
+
110
+ - `claude-code`
111
+ - `codex`
112
+ - `cursor`
113
+ - `vscode-copilot`
114
+ - `opencode`
115
+ - `all`
116
+
117
+ ## Commands
118
+
119
+ ```sh
120
+ unlegacy setup
121
+ unlegacy doctor
122
+ unlegacy install-skills --client codex
123
+ unlegacy status
124
+ ```
125
+
126
+ ## Development
127
+
128
+ ```sh
129
+ uv run pytest
130
+ ```
@@ -0,0 +1,111 @@
1
+ # unlegacy-cli
2
+
3
+ Installable CLI for connecting local coding agents to Unlegacy MCP.
4
+
5
+ Unlegacy generates repository documentation and knowledge graph data in the
6
+ platform. This CLI configures external coding agents so they can consume that
7
+ MCP surface.
8
+
9
+ ## Install
10
+
11
+ Published package:
12
+
13
+ ```sh
14
+ uv tool install unlegacy-cli
15
+ ```
16
+
17
+ or:
18
+
19
+ ```sh
20
+ pip install unlegacy-cli
21
+ ```
22
+
23
+ After install, the command is saved on the user's machine as:
24
+
25
+ ```sh
26
+ unlegacy --help
27
+ ```
28
+
29
+ From a checkout:
30
+
31
+ ```sh
32
+ pip install .
33
+ ```
34
+
35
+ or:
36
+
37
+ ```sh
38
+ uv tool install .
39
+ ```
40
+
41
+ One-shot setup without preinstalling:
42
+
43
+ ```sh
44
+ uv tool run --from unlegacy-cli unlegacy setup --token mcp_live_xxx --endpoint https://app.unlegacy.ai/ai/mcp
45
+ ```
46
+
47
+ ## Distribution
48
+
49
+ Build the package artifacts:
50
+
51
+ ```sh
52
+ uv build
53
+ ```
54
+
55
+ The build creates `dist/unlegacy_cli-<version>.tar.gz` and
56
+ `dist/unlegacy_cli-<version>-py3-none-any.whl`. Publish those artifacts to the
57
+ Python package index used by Unlegacy clients.
58
+
59
+ ## Setup
60
+
61
+ ```sh
62
+ unlegacy setup
63
+ ```
64
+
65
+ The flow:
66
+
67
+ 1. Shows the Unlegacy logo.
68
+ 2. Prompts for the MCP token from the Unlegacy frontend.
69
+ 3. Prompts for the MCP endpoint. Press Enter to use the local app endpoint: `http://localhost:5173/ai/mcp`.
70
+ 4. Lets the user select supported clients.
71
+ 5. Writes or merges MCP config.
72
+ 6. Installs the packaged `unlegacy` skill.
73
+ 7. Runs `unlegacy doctor`.
74
+
75
+ Non-interactive usage:
76
+
77
+ ```sh
78
+ UNLEGACY_MCP_TOKEN=mcp_live_xxx \
79
+ unlegacy setup --client claude-code --client codex --endpoint https://app.unlegacy.ai/ai/mcp
80
+ ```
81
+
82
+ For local development:
83
+
84
+ ```sh
85
+ UNLEGACY_MCP_TOKEN=mcp_live_xxx \
86
+ unlegacy setup --client claude-code --client codex --endpoint http://localhost:5173/ai/mcp
87
+ ```
88
+
89
+ Supported client values:
90
+
91
+ - `claude-code`
92
+ - `codex`
93
+ - `cursor`
94
+ - `vscode-copilot`
95
+ - `opencode`
96
+ - `all`
97
+
98
+ ## Commands
99
+
100
+ ```sh
101
+ unlegacy setup
102
+ unlegacy doctor
103
+ unlegacy install-skills --client codex
104
+ unlegacy status
105
+ ```
106
+
107
+ ## Development
108
+
109
+ ```sh
110
+ uv run pytest
111
+ ```
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "unlegacy-cli"
7
+ version = "0.1.0"
8
+ description = "Unlegacy setup CLI for external coding agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ authors = [{ name = "Unlegacy" }]
12
+ dependencies = []
13
+ keywords = ["unlegacy", "mcp", "cli", "coding-agents"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Topic :: Software Development",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://app.unlegacy.ai"
25
+ Repository = "https://github.com/unlegacy-ai/unlegacy-core"
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8.0"]
29
+
30
+ [project.scripts]
31
+ unlegacy = "unlegacy_cli.cli:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.setuptools.package-data]
37
+ unlegacy_cli = ["assets/skills/unlegacy/*"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Unlegacy CLI package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from unlegacy_cli.cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from unlegacy_cli.domain.models import ClientId, McpServerConfig
9
+
10
+
11
+ def _json_object(existing: str) -> dict[str, object]:
12
+ if not existing.strip():
13
+ return {}
14
+ try:
15
+ parsed = json.loads(existing)
16
+ except json.JSONDecodeError as exc:
17
+ raise ValueError(f"existing JSON config is invalid: {exc}") from exc
18
+ if not isinstance(parsed, dict):
19
+ raise ValueError("existing JSON config must be an object")
20
+ return parsed
21
+
22
+
23
+ def _dump_json(data: dict[str, object]) -> str:
24
+ return json.dumps(data, indent=2, sort_keys=True) + "\n"
25
+
26
+
27
+ def _merge_mcp_servers(existing: str, config: McpServerConfig, *, key: str, include_type: bool) -> str:
28
+ data = _json_object(existing)
29
+ servers = data.setdefault(key, {})
30
+ if not isinstance(servers, dict):
31
+ raise ValueError(f"existing {key} must be an object")
32
+ server: dict[str, object] = {
33
+ "url": config.normalized_endpoint(),
34
+ "headers": {"Authorization": config.authorization_header()},
35
+ }
36
+ if include_type:
37
+ server["type"] = "http"
38
+ servers["unlegacy"] = server
39
+ return _dump_json(data)
40
+
41
+
42
+ def build_claude_mcp_json(existing: str, config: McpServerConfig) -> str:
43
+ return _merge_mcp_servers(existing, config, key="mcpServers", include_type=True)
44
+
45
+
46
+ def build_cursor_mcp_json(existing: str, config: McpServerConfig) -> str:
47
+ return _merge_mcp_servers(existing, config, key="mcpServers", include_type=False)
48
+
49
+
50
+ def build_vscode_mcp_json(existing: str, config: McpServerConfig) -> str:
51
+ return _merge_mcp_servers(existing, config, key="servers", include_type=True)
52
+
53
+
54
+ def build_opencode_mcp_json(existing: str, config: McpServerConfig) -> str:
55
+ return _merge_mcp_servers(existing, config, key="mcpServers", include_type=True)
56
+
57
+
58
+ def _toml_quote(value: str) -> str:
59
+ return json.dumps(value)
60
+
61
+
62
+ def _codex_unlegacy_block(config: McpServerConfig) -> str:
63
+ auth = f"Bearer {config.token}"
64
+ return (
65
+ "[mcp_servers.unlegacy]\n"
66
+ f"url = {_toml_quote(config.normalized_endpoint())}\n"
67
+ f"http_headers = {{ Authorization = {_toml_quote(auth)} }}\n"
68
+ "startup_timeout_sec = 20\n"
69
+ "tool_timeout_sec = 60\n"
70
+ "enabled = true\n"
71
+ )
72
+
73
+
74
+ def build_codex_config_toml(existing: str, config: McpServerConfig) -> str:
75
+ cleaned = re.sub(
76
+ r"(?ms)^# BEGIN UNLEGACY MCP\n.*?^# END UNLEGACY MCP\n?",
77
+ "",
78
+ existing,
79
+ )
80
+ cleaned = re.sub(
81
+ r"(?ms)^\[mcp_servers\.unlegacy\]\n(?:(?!^\[).*\n?)*",
82
+ "",
83
+ cleaned,
84
+ )
85
+ prefix = cleaned.rstrip()
86
+ block = f"# BEGIN UNLEGACY MCP\n{_codex_unlegacy_block(config)}# END UNLEGACY MCP\n"
87
+ return f"{prefix}\n\n{block}" if prefix else block
88
+
89
+
90
+ def read_text(path: Path) -> str:
91
+ if not path.exists():
92
+ return ""
93
+ return path.read_text(encoding="utf-8")
94
+
95
+
96
+ def atomic_write(path: Path, content: str) -> None:
97
+ path.parent.mkdir(parents=True, exist_ok=True)
98
+ temp = path.with_suffix(f"{path.suffix}.tmp")
99
+ temp.write_text(content, encoding="utf-8")
100
+ os.replace(temp, path)
101
+
102
+
103
+ class FileSystemClientConfigStore:
104
+ def __init__(self, *, home: Path | None = None, project_dir: Path | None = None) -> None:
105
+ self._home = home or Path.home()
106
+ self._project_dir = project_dir or Path.cwd()
107
+
108
+ def configure(self, client: ClientId, config: McpServerConfig, *, dry_run: bool) -> str:
109
+ path, builder = self._target(client)
110
+ existing = read_text(path)
111
+ content = builder(existing, config)
112
+ if not dry_run:
113
+ if path.exists():
114
+ backup = path.with_suffix(f"{path.suffix}.bak")
115
+ backup.write_text(existing, encoding="utf-8")
116
+ atomic_write(path, content)
117
+ action = "would write" if dry_run else "wrote"
118
+ return f"{client.label}: {action} {path}. {reload_instruction(client)}"
119
+
120
+ def has_config(self, client: ClientId, config: McpServerConfig) -> bool:
121
+ path, _ = self._target(client)
122
+ existing = read_text(path)
123
+ if not existing:
124
+ return False
125
+ marker = config.normalized_endpoint()
126
+ return "unlegacy" in existing and marker in existing
127
+
128
+ def _target(self, client: ClientId):
129
+ if client == ClientId.CLAUDE_CODE:
130
+ return self._project_dir / ".mcp.json", build_claude_mcp_json
131
+ if client == ClientId.CODEX:
132
+ return self._home / ".codex" / "config.toml", build_codex_config_toml
133
+ if client == ClientId.CURSOR:
134
+ return self._project_dir / ".cursor" / "mcp.json", build_cursor_mcp_json
135
+ if client == ClientId.VSCODE_COPILOT:
136
+ return self._project_dir / ".vscode" / "mcp.json", build_vscode_mcp_json
137
+ if client == ClientId.OPENCODE:
138
+ return self._project_dir / ".opencode" / "mcp.json", build_opencode_mcp_json
139
+ raise ValueError(f"unsupported client: {client}")
140
+
141
+
142
+ def reload_instruction(client: ClientId) -> str:
143
+ if client == ClientId.CLAUDE_CODE:
144
+ return "Restart Claude Code to load the MCP server."
145
+ if client == ClientId.CODEX:
146
+ return "Restart Codex or open a new Codex session, then use /mcp to inspect the server."
147
+ if client == ClientId.CURSOR:
148
+ return "Reload the Cursor window to activate the MCP server."
149
+ if client == ClientId.VSCODE_COPILOT:
150
+ return "Reload VS Code or restart the Extension Host to activate the MCP server."
151
+ if client == ClientId.OPENCODE:
152
+ return "Restart OpenCode to activate the MCP server."
153
+ return "Restart the client to activate the MCP server."
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import json
5
+ import urllib.error
6
+ import urllib.request
7
+ from typing import Any
8
+
9
+ from unlegacy_cli.domain.models import McpServerConfig
10
+
11
+ PROTOCOL_VERSION = "2025-06-18"
12
+
13
+
14
+ class HttpMcpGateway:
15
+ def __init__(self, *, timeout_seconds: float = 20.0) -> None:
16
+ self._timeout_seconds = timeout_seconds
17
+
18
+ def validate(self, config: McpServerConfig) -> None:
19
+ self.list_tools(config)
20
+
21
+ def list_tools(self, config: McpServerConfig) -> list[str]:
22
+ session = _JsonRpcSession(config, timeout_seconds=self._timeout_seconds)
23
+ init = session.request(
24
+ "initialize",
25
+ {
26
+ "protocolVersion": PROTOCOL_VERSION,
27
+ "capabilities": {"tools": {}},
28
+ "clientInfo": {"name": "unlegacy-cli", "version": "0.1.0"},
29
+ },
30
+ )
31
+ server_version = init.get("protocolVersion")
32
+ if server_version != PROTOCOL_VERSION:
33
+ raise ValueError(
34
+ f"MCP protocol mismatch: server speaks {server_version!r}, CLI speaks {PROTOCOL_VERSION!r}"
35
+ )
36
+ session.notify("notifications/initialized", {})
37
+ result = session.request("tools/list", {})
38
+ tools = result.get("tools", [])
39
+ if not isinstance(tools, list):
40
+ raise ValueError("MCP tools/list returned an invalid response")
41
+ return [str(tool.get("name")) for tool in tools if isinstance(tool, dict)]
42
+
43
+ def list_repos(self, config: McpServerConfig) -> list[dict[str, object]]:
44
+ session = _JsonRpcSession(config, timeout_seconds=self._timeout_seconds)
45
+ session.request(
46
+ "initialize",
47
+ {
48
+ "protocolVersion": PROTOCOL_VERSION,
49
+ "capabilities": {"tools": {}},
50
+ "clientInfo": {"name": "unlegacy-cli", "version": "0.1.0"},
51
+ },
52
+ )
53
+ session.notify("notifications/initialized", {})
54
+ result = session.request("tools/call", {"name": "list_repos", "arguments": {}})
55
+ text = _tool_text(result)
56
+ try:
57
+ rows = json.loads(text)
58
+ except json.JSONDecodeError:
59
+ return []
60
+ return rows if isinstance(rows, list) else []
61
+
62
+
63
+ class _JsonRpcSession:
64
+ def __init__(self, config: McpServerConfig, *, timeout_seconds: float) -> None:
65
+ self._config = config
66
+ self._timeout_seconds = timeout_seconds
67
+ self._ids = itertools.count(1)
68
+ self._session_id: str | None = None
69
+
70
+ def request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
71
+ rid = next(self._ids)
72
+ body = {"jsonrpc": "2.0", "id": rid, "method": method, "params": params}
73
+ response = self._post(body)
74
+ if "error" in response:
75
+ error = response["error"]
76
+ if isinstance(error, dict):
77
+ raise ValueError(f"{method}: {error.get('message', 'MCP error')}")
78
+ raise ValueError(f"{method}: MCP error")
79
+ result = response.get("result", {})
80
+ return result if isinstance(result, dict) else {}
81
+
82
+ def notify(self, method: str, params: dict[str, Any]) -> None:
83
+ self._post({"jsonrpc": "2.0", "method": method, "params": params}, expect_response=False)
84
+
85
+ def _post(self, body: dict[str, Any], *, expect_response: bool = True) -> dict[str, Any]:
86
+ headers = {
87
+ "Accept": "application/json, text/event-stream",
88
+ "Content-Type": "application/json",
89
+ "Authorization": self._config.authorization_header(),
90
+ }
91
+ if self._session_id:
92
+ headers["Mcp-Session-Id"] = self._session_id
93
+ request = urllib.request.Request(
94
+ self._config.normalized_endpoint(),
95
+ data=json.dumps(body).encode("utf-8"),
96
+ headers=headers,
97
+ method="POST",
98
+ )
99
+ try:
100
+ with urllib.request.urlopen(request, timeout=self._timeout_seconds) as response:
101
+ self._session_id = response.headers.get("Mcp-Session-Id") or self._session_id
102
+ payload = response.read().decode("utf-8")
103
+ content_type = response.headers.get("content-type", "")
104
+ except urllib.error.HTTPError as exc:
105
+ if exc.code == 401:
106
+ raise ValueError("invalid or expired MCP token") from exc
107
+ raise ValueError(f"MCP request failed with HTTP {exc.code}") from exc
108
+ except urllib.error.URLError as exc:
109
+ raise ValueError(f"MCP server is unreachable: {exc.reason}") from exc
110
+ if not expect_response or not payload.strip():
111
+ return {}
112
+ return _decode_payload(payload, content_type)
113
+
114
+
115
+ def _decode_payload(payload: str, content_type: str) -> dict[str, Any]:
116
+ if "text/event-stream" in content_type:
117
+ for line in payload.splitlines():
118
+ if line.startswith("data:"):
119
+ data = line.removeprefix("data:").strip()
120
+ if data:
121
+ parsed = json.loads(data)
122
+ return parsed if isinstance(parsed, dict) else {}
123
+ return {}
124
+ parsed = json.loads(payload)
125
+ return parsed if isinstance(parsed, dict) else {}
126
+
127
+
128
+ def _tool_text(result: dict[str, Any]) -> str:
129
+ parts = result.get("content", [])
130
+ if not isinstance(parts, list):
131
+ return ""
132
+ return "\n".join(
133
+ str(part.get("text", ""))
134
+ for part in parts
135
+ if isinstance(part, dict) and part.get("type") == "text"
136
+ )
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import shutil
5
+ from importlib import resources
6
+ from pathlib import Path
7
+
8
+ from unlegacy_cli.domain.models import ClientId, SkillPackage
9
+
10
+
11
+ class PackagedSkillRepository:
12
+ def load(self) -> SkillPackage:
13
+ resource = resources.files("unlegacy_cli").joinpath("assets/skills/unlegacy/SKILL.md")
14
+ content = resource.read_text(encoding="utf-8")
15
+ name = _frontmatter_value(content, "name")
16
+ version = _frontmatter_value(content, "version")
17
+ for required in ["list_repos", "repo_info", "read_doc", "Do not guess"]:
18
+ if required.lower() not in content.lower():
19
+ raise ValueError(f"packaged skill missing required guidance: {required}")
20
+ return SkillPackage(name=name, version=version, content=content)
21
+
22
+
23
+ def _frontmatter_value(content: str, key: str) -> str:
24
+ match = re.search(rf"^{re.escape(key)}:\s*(.+)$", content, re.MULTILINE)
25
+ if not match:
26
+ raise ValueError(f"packaged skill missing {key} frontmatter")
27
+ return match.group(1).strip().strip("\"'")
28
+
29
+
30
+ class FileSystemSkillInstaller:
31
+ def __init__(self, *, repository: PackagedSkillRepository | None = None, home: Path | None = None) -> None:
32
+ self._repository = repository or PackagedSkillRepository()
33
+ self._home = home or Path.home()
34
+
35
+ def install(self, client: ClientId, *, dry_run: bool) -> str:
36
+ skill = self._repository.load()
37
+ target = self._target(client, skill.name)
38
+ if not dry_run:
39
+ target.mkdir(parents=True, exist_ok=True)
40
+ (target / "SKILL.md").write_text(skill.content, encoding="utf-8")
41
+ return f"{client.label}: {'would install' if dry_run else 'installed'} skill {skill.name}@{skill.version} to {target}"
42
+
43
+ def is_installed(self, client: ClientId) -> bool:
44
+ skill = self._repository.load()
45
+ path = self._target(client, skill.name) / "SKILL.md"
46
+ if not path.exists():
47
+ return False
48
+ try:
49
+ content = path.read_text(encoding="utf-8")
50
+ except OSError:
51
+ return False
52
+ return f"version: {skill.version}" in content and "name: unlegacy" in content
53
+
54
+ def version(self) -> str:
55
+ return self._repository.load().version
56
+
57
+ def _target(self, client: ClientId, name: str) -> Path:
58
+ if client == ClientId.CLAUDE_CODE:
59
+ return self._home / ".claude" / "skills" / name
60
+ if client == ClientId.CODEX:
61
+ return self._home / ".codex" / "skills" / name
62
+ if client == ClientId.CURSOR:
63
+ return self._home / ".cursor" / "skills" / name
64
+ if client == ClientId.VSCODE_COPILOT:
65
+ return self._home / ".vscode" / "extensions" / "unlegacy" / "skills" / name
66
+ if client == ClientId.OPENCODE:
67
+ return self._home / ".config" / "opencode" / "skills" / name
68
+ raise ValueError(f"unsupported client: {client}")
69
+
70
+
71
+ def copy_packaged_skill_to(path: Path) -> None:
72
+ resource_dir = resources.files("unlegacy_cli").joinpath("assets/skills/unlegacy")
73
+ if path.exists():
74
+ shutil.rmtree(path)
75
+ path.mkdir(parents=True)
76
+ for item in resource_dir.iterdir():
77
+ if item.is_file():
78
+ (path / item.name).write_text(item.read_text(encoding="utf-8"), encoding="utf-8")