kitup-sdk 0.1.2__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,16 @@
1
+ *.log
2
+ .DS_Store
3
+ .env
4
+ .env.*
5
+ !.env.example
6
+ .local/
7
+
8
+ node_modules/
9
+ dist/
10
+ coverage/
11
+ *.tsbuildinfo
12
+
13
+ *.test
14
+ coverage.out
15
+
16
+ target/
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: kitup-sdk
3
+ Version: 0.1.2
4
+ Summary: Shared installer SDK for bundled Agent Skills.
5
+ License: MIT
6
+ Keywords: agent,installer,sdk,skills
7
+ Requires-Python: >=3.14
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=8.3.0; extra == 'dev'
10
+ Requires-Dist: ruff>=0.12.0; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # kitup Python SDK
14
+
15
+ Shared installer SDK for bundled Agent Skills.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install kitup-sdk
21
+ ```
22
+
23
+ ## Use
24
+
25
+ Use the workflow API for user-facing install commands:
26
+
27
+ ```python
28
+ from kitup import (
29
+ BaseOptions,
30
+ InstallOptions,
31
+ InstallWorkflowOptions,
32
+ directory_bundle,
33
+ run_bundled_skill_install,
34
+ )
35
+
36
+ workflow = run_bundled_skill_install(
37
+ InstallWorkflowOptions(
38
+ install=InstallOptions(
39
+ base=BaseOptions(),
40
+ app_id="mycli",
41
+ skill_bundle=directory_bundle("./skills/mycli"),
42
+ scope="user",
43
+ ),
44
+ stdin_tty=True,
45
+ prompt_scope=True,
46
+ )
47
+ )
48
+ ```
49
+
50
+ Call `install_bundled_skill` when your CLI already knows the target scope and agents and does not need the interactive workflow surface.
@@ -0,0 +1,38 @@
1
+ # kitup Python SDK
2
+
3
+ Shared installer SDK for bundled Agent Skills.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install kitup-sdk
9
+ ```
10
+
11
+ ## Use
12
+
13
+ Use the workflow API for user-facing install commands:
14
+
15
+ ```python
16
+ from kitup import (
17
+ BaseOptions,
18
+ InstallOptions,
19
+ InstallWorkflowOptions,
20
+ directory_bundle,
21
+ run_bundled_skill_install,
22
+ )
23
+
24
+ workflow = run_bundled_skill_install(
25
+ InstallWorkflowOptions(
26
+ install=InstallOptions(
27
+ base=BaseOptions(),
28
+ app_id="mycli",
29
+ skill_bundle=directory_bundle("./skills/mycli"),
30
+ scope="user",
31
+ ),
32
+ stdin_tty=True,
33
+ prompt_scope=True,
34
+ )
35
+ )
36
+ ```
37
+
38
+ Call `install_bundled_skill` when your CLI already knows the target scope and agents and does not need the interactive workflow surface.
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kitup-sdk"
7
+ version = "0.1.2"
8
+ description = "Shared installer SDK for bundled Agent Skills."
9
+ readme = "README.md"
10
+ requires-python = ">=3.14"
11
+ license = { text = "MIT" }
12
+ keywords = ["agent", "skills", "installer", "sdk"]
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "pytest>=8.3.0",
18
+ "ruff>=0.12.0",
19
+ ]
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=8.3.0",
24
+ "ruff>=0.12.0",
25
+ ]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/kitup"]
32
+
33
+ [tool.ruff]
34
+ target-version = "py314"
@@ -0,0 +1,95 @@
1
+ from .bundle import (
2
+ compute_bundle_content_hash,
3
+ directory_bundle,
4
+ files_bundle,
5
+ github_bundle,
6
+ validate_skill_bundle,
7
+ )
8
+ from .hosts import detect_hosts, load_host_spec, resolve_hosts
9
+ from .install import (
10
+ install_bundled_skill,
11
+ plan_bundled_skill,
12
+ resolve_install_targets,
13
+ uninstall_bundled_skill,
14
+ update_bundled_skill,
15
+ )
16
+ from .workflow import (
17
+ agent_selector_from_flags,
18
+ classify_install_workflow_exit,
19
+ install_flag_error,
20
+ install_workflow_error,
21
+ parse_install_flags,
22
+ parse_scope_flag,
23
+ resolve_install_selection,
24
+ run_bundled_skill_install,
25
+ run_bundled_skill_install_with_io,
26
+ )
27
+ from .types import (
28
+ BaseOptions,
29
+ GitHubBundleOptions,
30
+ Host,
31
+ HostSpec,
32
+ INSTALL_UX,
33
+ InstallOptions,
34
+ InstallReport,
35
+ InstallSelection,
36
+ InstallSelectionOptions,
37
+ InstallWorkflowExit,
38
+ InstallWorkflowOptions,
39
+ InstallWorkflowReport,
40
+ KitupError,
41
+ ParsedInstallFlags,
42
+ SkillFile,
43
+ TargetError,
44
+ TargetGroup,
45
+ TargetResult,
46
+ TargetStatus,
47
+ UninstallOptions,
48
+ UninstallReport,
49
+ )
50
+
51
+ __all__ = [
52
+ "BaseOptions",
53
+ "GitHubBundleOptions",
54
+ "Host",
55
+ "HostSpec",
56
+ "INSTALL_UX",
57
+ "InstallOptions",
58
+ "InstallReport",
59
+ "InstallSelection",
60
+ "InstallSelectionOptions",
61
+ "InstallWorkflowExit",
62
+ "InstallWorkflowOptions",
63
+ "InstallWorkflowReport",
64
+ "KitupError",
65
+ "ParsedInstallFlags",
66
+ "SkillFile",
67
+ "TargetError",
68
+ "TargetGroup",
69
+ "TargetResult",
70
+ "TargetStatus",
71
+ "UninstallOptions",
72
+ "UninstallReport",
73
+ "compute_bundle_content_hash",
74
+ "detect_hosts",
75
+ "directory_bundle",
76
+ "files_bundle",
77
+ "github_bundle",
78
+ "agent_selector_from_flags",
79
+ "classify_install_workflow_exit",
80
+ "install_bundled_skill",
81
+ "install_flag_error",
82
+ "install_workflow_error",
83
+ "load_host_spec",
84
+ "parse_install_flags",
85
+ "parse_scope_flag",
86
+ "plan_bundled_skill",
87
+ "resolve_hosts",
88
+ "resolve_install_selection",
89
+ "resolve_install_targets",
90
+ "run_bundled_skill_install",
91
+ "run_bundled_skill_install_with_io",
92
+ "uninstall_bundled_skill",
93
+ "update_bundled_skill",
94
+ "validate_skill_bundle",
95
+ ]
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import urllib.parse
6
+ import urllib.request
7
+
8
+ from ._paths import trim_github_path
9
+ from .types import GitHubBundleOptions, KitupError, SkillFile
10
+
11
+
12
+ def fetch_github_directory(options: GitHubBundleOptions) -> list[SkillFile]:
13
+ files, _ = fetch_github_directory_with_metadata(options)
14
+ return files
15
+
16
+
17
+ def fetch_github_directory_with_metadata(
18
+ options: GitHubBundleOptions,
19
+ ) -> tuple[list[SkillFile], dict[str, object]]:
20
+ root = trim_github_path(options.path)
21
+ if not options.owner or not options.repo or not root or not options.ref:
22
+ raise KitupError("invalid github bundle")
23
+
24
+ api_base = _env_base_url("KITUP_GITHUB_API_BASE_URL", "https://api.github.com")
25
+ raw_base = _env_base_url(
26
+ "KITUP_GITHUB_RAW_BASE_URL",
27
+ "https://raw.githubusercontent.com",
28
+ )
29
+
30
+ commit = github_json(
31
+ f"{api_base}/repos/{_encode_path_part(options.owner)}/"
32
+ f"{_encode_path_part(options.repo)}/commits/{_encode_path_part(options.ref)}"
33
+ )
34
+ resolved_commit = str(commit.get("sha") or "")
35
+ tree_sha = str(((commit.get("commit") or {}).get("tree") or {}).get("sha") or "")
36
+ if not resolved_commit or not tree_sha:
37
+ raise KitupError("invalid github commit")
38
+
39
+ tree = github_json(
40
+ f"{api_base}/repos/{_encode_path_part(options.owner)}/"
41
+ f"{_encode_path_part(options.repo)}/git/trees/{_encode_path_part(tree_sha)}"
42
+ "?recursive=1"
43
+ )
44
+
45
+ prefix = f"{root}/"
46
+ files: list[SkillFile] = []
47
+ for item in tree.get("tree") or []:
48
+ if not isinstance(item, dict):
49
+ continue
50
+ path = str(item.get("path") or "")
51
+ if item.get("type") != "blob" or not path.startswith(prefix):
52
+ continue
53
+ url = (
54
+ f"{raw_base}/{_encode_path_part(options.owner)}/"
55
+ f"{_encode_path_part(options.repo)}/"
56
+ f"{_encode_path_part(resolved_commit)}/{_encode_path(path)}"
57
+ )
58
+ files.append(
59
+ SkillFile(
60
+ path=path[len(prefix) :],
61
+ contents=github_bytes(url),
62
+ mode=0o755 if item.get("mode") == "100755" else 0o644,
63
+ )
64
+ )
65
+
66
+ if not files:
67
+ raise KitupError("github bundle path not found")
68
+
69
+ return files, {
70
+ "source": "github",
71
+ "source_id": f"github:{options.owner}/{options.repo}/{root}",
72
+ "version": options.ref,
73
+ "provenance": {
74
+ "owner": options.owner,
75
+ "repo": options.repo,
76
+ "path": root,
77
+ "ref": options.ref,
78
+ "resolvedCommit": resolved_commit,
79
+ },
80
+ }
81
+
82
+
83
+ def github_json(url: str) -> dict[str, object]:
84
+ with urllib.request.urlopen(_request(url), timeout=30) as response:
85
+ return json.loads(response.read().decode("utf-8"))
86
+
87
+
88
+ def github_bytes(url: str) -> bytes:
89
+ with urllib.request.urlopen(_request(url), timeout=30) as response:
90
+ return response.read()
91
+
92
+
93
+ def _request(url: str) -> urllib.request.Request:
94
+ return urllib.request.Request(url, headers={"User-Agent": "kitup"})
95
+
96
+
97
+ def _env_base_url(name: str, fallback: str) -> str:
98
+ value = os.environ.get(name, "").rstrip("/")
99
+ return value or fallback
100
+
101
+
102
+ def _encode_path(path: str) -> str:
103
+ return "/".join(_encode_path_part(part) for part in path.split("/"))
104
+
105
+
106
+ def _encode_path_part(part: str) -> str:
107
+ return urllib.parse.quote(part, safe="")
@@ -0,0 +1,3 @@
1
+ # Code generated from spec/hosts.json. DO NOT EDIT.
2
+
3
+ DEFAULT_HOSTS_SPEC_JSON = "{\"$schema\":\"./hosts.schema.json\",\"schemaVersion\":1,\"hosts\":[{\"id\":\"adal\",\"displayName\":\"AdaL\",\"projectSkillsDirs\":[\".adal/skills\"],\"userSkillsDirs\":[\"~/.adal/skills\"],\"detect\":[\"~/.adal\"],\"status\":\"community\"},{\"id\":\"aider-desk\",\"displayName\":\"AiderDesk\",\"projectSkillsDirs\":[\".aider-desk/skills\"],\"userSkillsDirs\":[\"~/.aider-desk/skills\"],\"detect\":[\"~/.aider-desk\"],\"status\":\"community\"},{\"id\":\"amp\",\"displayName\":\"Amp\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.config/agents/skills\"],\"detect\":[\"~/.config/amp\",\"~/.config/agents\"],\"status\":\"community\"},{\"id\":\"antigravity\",\"displayName\":\"Antigravity\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.gemini/antigravity/skills\"],\"detect\":[\"~/.gemini/antigravity\"],\"status\":\"community\"},{\"id\":\"antigravity-cli\",\"displayName\":\"Antigravity CLI\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.gemini/antigravity-cli/skills\"],\"detect\":[\"~/.gemini/antigravity-cli\"],\"status\":\"community\"},{\"id\":\"astrbot\",\"displayName\":\"AstrBot\",\"projectSkillsDirs\":[\"data/skills\"],\"userSkillsDirs\":[\"~/.astrbot/data/skills\"],\"detect\":[\"~/.astrbot\",\"data/skills\",\"~/.astrbot/data\"],\"status\":\"community\"},{\"id\":\"augment\",\"displayName\":\"Augment\",\"projectSkillsDirs\":[\".augment/skills\"],\"userSkillsDirs\":[\"~/.augment/skills\"],\"detect\":[\"~/.augment\"],\"status\":\"community\"},{\"id\":\"autohand-code\",\"displayName\":\"Autohand Code CLI\",\"projectSkillsDirs\":[\".autohand/skills\"],\"userSkillsDirs\":[\"~/.autohand/skills\"],\"detect\":[\"~/.autohand\"],\"status\":\"community\"},{\"id\":\"bob\",\"displayName\":\"IBM Bob\",\"projectSkillsDirs\":[\".bob/skills\"],\"userSkillsDirs\":[\"~/.bob/skills\"],\"detect\":[\"~/.bob\"],\"status\":\"community\"},{\"id\":\"claude-code\",\"displayName\":\"Claude Code\",\"projectSkillsDirs\":[\".claude/skills\"],\"userSkillsDirs\":[\"~/.claude/skills\"],\"detect\":[\"~/.claude\"],\"status\":\"verified\"},{\"id\":\"cline\",\"displayName\":\"Cline\",\"projectSkillsDirs\":[\".agents/skills\",\".cline/skills\",\".clinerules/skills\",\".claude/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\",\"~/.cline/skills\"],\"detect\":[\"~/.cline\",\"~/.agents\"],\"status\":\"documented\"},{\"id\":\"codearts-agent\",\"displayName\":\"CodeArts Agent\",\"projectSkillsDirs\":[\".codeartsdoer/skills\"],\"userSkillsDirs\":[\"~/.codeartsdoer/skills\"],\"detect\":[\"~/.codeartsdoer\"],\"status\":\"community\"},{\"id\":\"codebuddy\",\"displayName\":\"CodeBuddy\",\"projectSkillsDirs\":[\".codebuddy/skills\"],\"userSkillsDirs\":[\"~/.codebuddy/skills\"],\"detect\":[\"~/.codebuddy\",\".codebuddy\"],\"status\":\"community\"},{\"id\":\"codemaker\",\"displayName\":\"Codemaker\",\"projectSkillsDirs\":[\".codemaker/skills\"],\"userSkillsDirs\":[\"~/.codemaker/skills\"],\"detect\":[\"~/.codemaker\"],\"status\":\"community\"},{\"id\":\"codestudio\",\"displayName\":\"Code Studio\",\"projectSkillsDirs\":[\".codestudio/skills\"],\"userSkillsDirs\":[\"~/.codestudio/skills\"],\"detect\":[\"~/.codestudio\"],\"status\":\"community\"},{\"id\":\"codex\",\"displayName\":\"Codex\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\",\"~/.codex/skills\"],\"detect\":[\"~/.codex\",\"~/.agents/skills\",\"~/.agents\"],\"status\":\"verified\",\"notes\":[\"Keep both ~/.agents/skills and ~/.codex/skills for compatibility.\"]},{\"id\":\"command-code\",\"displayName\":\"Command Code\",\"projectSkillsDirs\":[\".commandcode/skills\"],\"userSkillsDirs\":[\"~/.commandcode/skills\"],\"detect\":[\"~/.commandcode\"],\"status\":\"community\"},{\"id\":\"continue\",\"displayName\":\"Continue\",\"projectSkillsDirs\":[\".continue/skills\"],\"userSkillsDirs\":[\"~/.continue/skills\"],\"detect\":[\"~/.continue\",\".continue\"],\"status\":\"community\"},{\"id\":\"cortex\",\"displayName\":\"Cortex Code\",\"projectSkillsDirs\":[\".cortex/skills\"],\"userSkillsDirs\":[\"~/.snowflake/cortex/skills\"],\"detect\":[\"~/.snowflake/cortex\"],\"status\":\"community\"},{\"id\":\"crush\",\"displayName\":\"Crush\",\"projectSkillsDirs\":[\".crush/skills\"],\"userSkillsDirs\":[\"~/.config/crush/skills\"],\"detect\":[\"~/.config/crush\"],\"status\":\"community\"},{\"id\":\"cursor\",\"displayName\":\"Cursor\",\"projectSkillsDirs\":[\".agents/skills\",\".cursor/skills\"],\"userSkillsDirs\":[\"~/.cursor/skills\",\"~/.agents/skills\"],\"detect\":[\"~/.cursor\",\"~/.agents\"],\"status\":\"documented\"},{\"id\":\"deepagents\",\"displayName\":\"Deep Agents\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.deepagents/agent/skills\"],\"detect\":[\"~/.deepagents\",\"~/.deepagents/agent\"],\"status\":\"community\"},{\"id\":\"devin\",\"displayName\":\"Devin for Terminal\",\"projectSkillsDirs\":[\".devin/skills\"],\"userSkillsDirs\":[\"~/.config/devin/skills\"],\"detect\":[\"~/.config/devin\"],\"status\":\"community\"},{\"id\":\"dexto\",\"displayName\":\"Dexto\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\"],\"detect\":[\"~/.dexto\",\"~/.agents\"],\"status\":\"community\"},{\"id\":\"droid\",\"displayName\":\"Droid\",\"projectSkillsDirs\":[\".factory/skills\"],\"userSkillsDirs\":[\"~/.factory/skills\"],\"detect\":[\"~/.factory\"],\"status\":\"community\"},{\"id\":\"eve\",\"displayName\":\"Eve\",\"projectSkillsDirs\":[\"agent/skills\"],\"userSkillsDirs\":[],\"detect\":[\"agent\",\"package.json\"],\"status\":\"community\",\"notes\":[\"Project-only host; userSkillsDirs is intentionally empty.\",\"Detect from Eve project shape; no global skill directory.\"]},{\"id\":\"firebender\",\"displayName\":\"Firebender\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.firebender/skills\"],\"detect\":[\"~/.firebender\"],\"status\":\"community\"},{\"id\":\"forgecode\",\"displayName\":\"ForgeCode\",\"projectSkillsDirs\":[\".forge/skills\"],\"userSkillsDirs\":[\"~/.forge/skills\"],\"detect\":[\"~/.forge\"],\"status\":\"community\"},{\"id\":\"gemini-cli\",\"displayName\":\"Gemini CLI\",\"projectSkillsDirs\":[\".agents/skills\",\".gemini/skills\"],\"userSkillsDirs\":[\"~/.gemini/skills\",\"~/.agents/skills\"],\"detect\":[\"~/.gemini\",\"~/.agents\"],\"status\":\"documented\"},{\"id\":\"github-copilot\",\"displayName\":\"GitHub Copilot\",\"projectSkillsDirs\":[\".agents/skills\",\".github/skills\",\".claude/skills\"],\"userSkillsDirs\":[\"~/.copilot/skills\",\"~/.agents/skills\",\"~/.claude/skills\"],\"detect\":[\"~/.copilot\",\"~/.agents\",\"~/.claude\"],\"status\":\"documented\"},{\"id\":\"goose\",\"displayName\":\"Goose\",\"projectSkillsDirs\":[\".goose/skills\"],\"userSkillsDirs\":[\"~/.config/goose/skills\"],\"detect\":[\"~/.config/goose\"],\"status\":\"community\"},{\"id\":\"hermes-agent\",\"displayName\":\"Hermes Agent\",\"projectSkillsDirs\":[\".hermes/skills\"],\"userSkillsDirs\":[\"~/.hermes/skills\"],\"detect\":[\"~/.hermes\"],\"status\":\"community\"},{\"id\":\"iflow-cli\",\"displayName\":\"iFlow CLI\",\"projectSkillsDirs\":[\".iflow/skills\"],\"userSkillsDirs\":[\"~/.iflow/skills\"],\"detect\":[\"~/.iflow\"],\"status\":\"community\"},{\"id\":\"inference-sh\",\"displayName\":\"inference.sh\",\"projectSkillsDirs\":[\".inferencesh/skills\"],\"userSkillsDirs\":[\"~/.inferencesh/skills\"],\"detect\":[\"~/.inferencesh\"],\"status\":\"community\"},{\"id\":\"jazz\",\"displayName\":\"Jazz\",\"projectSkillsDirs\":[\".jazz/skills\"],\"userSkillsDirs\":[\"~/.jazz/skills\"],\"detect\":[\"~/.jazz\",\".jazz\"],\"status\":\"community\"},{\"id\":\"junie\",\"displayName\":\"Junie\",\"projectSkillsDirs\":[\".junie/skills\"],\"userSkillsDirs\":[\"~/.junie/skills\"],\"detect\":[\"~/.junie\"],\"status\":\"community\"},{\"id\":\"kilo\",\"displayName\":\"Kilo Code\",\"projectSkillsDirs\":[\".kilocode/skills\"],\"userSkillsDirs\":[\"~/.kilocode/skills\"],\"detect\":[\"~/.kilocode\"],\"status\":\"community\"},{\"id\":\"kimi-cli\",\"displayName\":\"Kimi Code CLI\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.config/agents/skills\",\"~/.agents/skills\"],\"detect\":[\"~/.config/agents\",\"~/.kimi-code\",\"~/.kimi\",\"~/.agents\"],\"status\":\"community\",\"notes\":[\"kimi-code-cli is an alias for the same Kimi Code CLI path family.\"],\"aliases\":[\"kimi-code-cli\"]},{\"id\":\"kiro-cli\",\"displayName\":\"Kiro CLI\",\"projectSkillsDirs\":[\".kiro/skills\"],\"userSkillsDirs\":[\"~/.kiro/skills\"],\"detect\":[\"~/.kiro\"],\"status\":\"community\"},{\"id\":\"kode\",\"displayName\":\"Kode\",\"projectSkillsDirs\":[\".kode/skills\"],\"userSkillsDirs\":[\"~/.kode/skills\"],\"detect\":[\"~/.kode\"],\"status\":\"community\"},{\"id\":\"lingma\",\"displayName\":\"Lingma\",\"projectSkillsDirs\":[\".lingma/skills\"],\"userSkillsDirs\":[\"~/.lingma/skills\"],\"detect\":[\"~/.lingma\"],\"status\":\"community\"},{\"id\":\"loaf\",\"displayName\":\"Loaf\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\"],\"detect\":[\"~/.loaf\",\"~/.agents\"],\"status\":\"community\"},{\"id\":\"mcpjam\",\"displayName\":\"MCPJam\",\"projectSkillsDirs\":[\".mcpjam/skills\"],\"userSkillsDirs\":[\"~/.mcpjam/skills\"],\"detect\":[\"~/.mcpjam\"],\"status\":\"community\"},{\"id\":\"mistral-vibe\",\"displayName\":\"Mistral Vibe\",\"projectSkillsDirs\":[\".vibe/skills\"],\"userSkillsDirs\":[\"~/.vibe/skills\"],\"detect\":[\"~/.vibe\"],\"status\":\"community\"},{\"id\":\"moxby\",\"displayName\":\"Moxby\",\"projectSkillsDirs\":[\".moxby/skills\"],\"userSkillsDirs\":[\"~/.moxby/skills\"],\"detect\":[\"~/.moxby\"],\"status\":\"community\"},{\"id\":\"mux\",\"displayName\":\"Mux\",\"projectSkillsDirs\":[\".mux/skills\"],\"userSkillsDirs\":[\"~/.mux/skills\"],\"detect\":[\"~/.mux\"],\"status\":\"community\"},{\"id\":\"neovate\",\"displayName\":\"Neovate\",\"projectSkillsDirs\":[\".neovate/skills\"],\"userSkillsDirs\":[\"~/.neovate/skills\"],\"detect\":[\"~/.neovate\"],\"status\":\"community\"},{\"id\":\"ona\",\"displayName\":\"Ona\",\"projectSkillsDirs\":[\".ona/skills\"],\"userSkillsDirs\":[\"~/.ona/skills\"],\"detect\":[\"~/.ona\"],\"status\":\"community\"},{\"id\":\"openclaw\",\"displayName\":\"OpenClaw\",\"projectSkillsDirs\":[\"skills\"],\"userSkillsDirs\":[\"~/.openclaw/skills\"],\"detect\":[\"~/.openclaw\",\"~/.clawdbot\",\"~/.moltbot\"],\"status\":\"community\"},{\"id\":\"opencode\",\"displayName\":\"OpenCode\",\"projectSkillsDirs\":[\".agents/skills\",\".opencode/skills\",\".claude/skills\"],\"userSkillsDirs\":[\"~/.config/opencode/skills\",\"~/.agents/skills\",\"~/.claude/skills\"],\"detect\":[\"~/.config/opencode\",\"~/.agents\",\"~/.claude\"],\"status\":\"verified\"},{\"id\":\"openhands\",\"displayName\":\"OpenHands\",\"projectSkillsDirs\":[\".openhands/skills\"],\"userSkillsDirs\":[\"~/.openhands/skills\"],\"detect\":[\"~/.openhands\"],\"status\":\"community\"},{\"id\":\"pi\",\"displayName\":\"Pi\",\"projectSkillsDirs\":[\".pi/skills\"],\"userSkillsDirs\":[\"~/.pi/agent/skills\"],\"detect\":[\"~/.pi/agent\"],\"status\":\"community\"},{\"id\":\"pochi\",\"displayName\":\"Pochi\",\"projectSkillsDirs\":[\".pochi/skills\"],\"userSkillsDirs\":[\"~/.pochi/skills\"],\"detect\":[\"~/.pochi\"],\"status\":\"community\"},{\"id\":\"promptscript\",\"displayName\":\"PromptScript\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[],\"detect\":[\".promptscript\",\"promptscript.yaml\"],\"status\":\"community\",\"notes\":[\"Project-only host; userSkillsDirs is intentionally empty.\"]},{\"id\":\"qoder\",\"displayName\":\"Qoder\",\"projectSkillsDirs\":[\".qoder/skills\"],\"userSkillsDirs\":[\"~/.qoder/skills\"],\"detect\":[\"~/.qoder\"],\"status\":\"community\"},{\"id\":\"qoder-cn\",\"displayName\":\"Qoder CN\",\"projectSkillsDirs\":[\".qoder/skills\"],\"userSkillsDirs\":[\"~/.qoder-cn/skills\"],\"detect\":[\"~/.qoder-cn\"],\"status\":\"community\"},{\"id\":\"qwen-code\",\"displayName\":\"Qwen Code\",\"projectSkillsDirs\":[\".qwen/skills\"],\"userSkillsDirs\":[\"~/.qwen/skills\"],\"detect\":[\"~/.qwen\"],\"status\":\"community\"},{\"id\":\"reasonix\",\"displayName\":\"Reasonix\",\"projectSkillsDirs\":[\".reasonix/skills\"],\"userSkillsDirs\":[\"~/.reasonix/skills\"],\"detect\":[\"~/.reasonix\"],\"status\":\"community\"},{\"id\":\"replit\",\"displayName\":\"Replit\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.config/agents/skills\"],\"detect\":[\".replit\",\"~/.config/agents\"],\"status\":\"community\"},{\"id\":\"roo\",\"displayName\":\"Roo Code\",\"aliases\":[\"roo-code\"],\"projectSkillsDirs\":[\".roo/skills\",\".agents/skills\"],\"userSkillsDirs\":[\"~/.roo/skills\",\"~/.agents/skills\"],\"detect\":[\"~/.roo\",\"~/.agents\"],\"status\":\"documented\"},{\"id\":\"rovodev\",\"displayName\":\"Rovo Dev\",\"projectSkillsDirs\":[\".rovodev/skills\"],\"userSkillsDirs\":[\"~/.rovodev/skills\"],\"detect\":[\"~/.rovodev\"],\"status\":\"community\"},{\"id\":\"tabnine-cli\",\"displayName\":\"Tabnine CLI\",\"projectSkillsDirs\":[\".tabnine/agent/skills\"],\"userSkillsDirs\":[\"~/.tabnine/agent/skills\"],\"detect\":[\"~/.tabnine\",\"~/.tabnine/agent\"],\"status\":\"community\"},{\"id\":\"terramind\",\"displayName\":\"Terramind\",\"projectSkillsDirs\":[\".terramind/skills\"],\"userSkillsDirs\":[\"~/.terramind/skills\"],\"detect\":[\"~/.terramind\"],\"status\":\"community\"},{\"id\":\"tinycloud\",\"displayName\":\"Tinycloud\",\"projectSkillsDirs\":[\".tinycloud/skills\"],\"userSkillsDirs\":[\"~/.tinycloud/skills\"],\"detect\":[\"~/.tinycloud\"],\"status\":\"community\"},{\"id\":\"trae\",\"displayName\":\"Trae\",\"projectSkillsDirs\":[\".trae/skills\"],\"userSkillsDirs\":[\"~/.trae/skills\"],\"detect\":[\"~/.trae\"],\"status\":\"community\"},{\"id\":\"trae-cn\",\"displayName\":\"Trae CN\",\"projectSkillsDirs\":[\".trae/skills\"],\"userSkillsDirs\":[\"~/.trae-cn/skills\"],\"detect\":[\"~/.trae-cn\"],\"status\":\"community\"},{\"id\":\"universal\",\"displayName\":\"Universal\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\",\"~/.config/agents/skills\"],\"detect\":[\"~/.agents\",\"~/.config/agents\"],\"status\":\"community\"},{\"id\":\"warp\",\"displayName\":\"Warp\",\"projectSkillsDirs\":[\".agents/skills\",\".warp/skills\",\".claude/skills\",\".codex/skills\",\".cursor/skills\",\".gemini/skills\",\".copilot/skills\",\".factory/skills\",\".github/skills\",\".opencode/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\",\"~/.warp/skills\",\"~/.claude/skills\",\"~/.codex/skills\",\"~/.cursor/skills\",\"~/.gemini/skills\",\"~/.copilot/skills\",\"~/.factory/skills\",\"~/.github/skills\",\"~/.opencode/skills\"],\"detect\":[\"~/.warp\",\"~/.agents\",\"~/.claude\",\"~/.codex\",\"~/.cursor\",\"~/.gemini\",\"~/.copilot\",\"~/.factory\",\"~/.github\",\"~/.opencode\"],\"status\":\"documented\"},{\"id\":\"windsurf\",\"displayName\":\"Windsurf\",\"projectSkillsDirs\":[\".windsurf/skills\"],\"userSkillsDirs\":[\"~/.codeium/windsurf/skills\"],\"detect\":[\"~/.codeium/windsurf\"],\"status\":\"community\"},{\"id\":\"zed\",\"displayName\":\"Zed\",\"projectSkillsDirs\":[\".agents/skills\"],\"userSkillsDirs\":[\"~/.agents/skills\"],\"detect\":[\"~/.config/zed\",\"~/.agents\"],\"status\":\"community\"},{\"id\":\"zencoder\",\"displayName\":\"Zencoder\",\"projectSkillsDirs\":[\".zencoder/skills\"],\"userSkillsDirs\":[\"~/.zencoder/skills\"],\"detect\":[\"~/.zencoder\"],\"status\":\"community\"},{\"id\":\"zenflow\",\"displayName\":\"Zenflow\",\"projectSkillsDirs\":[\".zencoder/skills\"],\"userSkillsDirs\":[\"~/.zencoder/skills\"],\"detect\":[\"~/.zencoder\"],\"status\":\"community\"}]}"
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def write_install_metadata(
8
+ target_dir: Path,
9
+ *,
10
+ app_id: str,
11
+ skill_name: str,
12
+ digest: str,
13
+ source: str,
14
+ source_id: str | None = None,
15
+ version: str | None = None,
16
+ provenance: dict[str, object] | None = None,
17
+ ) -> None:
18
+ payload = {
19
+ "schemaVersion": 1,
20
+ "appId": app_id,
21
+ "skillName": skill_name,
22
+ "source": source,
23
+ "hash": digest,
24
+ }
25
+ if source_id is not None:
26
+ payload["sourceId"] = source_id
27
+ if version is not None:
28
+ payload["version"] = version
29
+ if provenance is not None:
30
+ payload["provenance"] = provenance
31
+ (target_dir / ".kitup.json").write_text(
32
+ json.dumps(payload, indent=2) + "\n", encoding="utf-8"
33
+ )
34
+
35
+
36
+ def read_install_metadata(target_dir: Path) -> dict[str, object] | None:
37
+ metadata_file = target_dir / ".kitup.json"
38
+ if not metadata_file.exists():
39
+ return None
40
+ try:
41
+ payload = json.loads(metadata_file.read_text(encoding="utf-8"))
42
+ except OSError, ValueError:
43
+ return None
44
+ if not isinstance(payload, dict):
45
+ return None
46
+ return payload
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from .types import KitupError
6
+
7
+
8
+ def skip_name(name: str) -> bool:
9
+ return (
10
+ name == ".git"
11
+ or name == ".kitup.json"
12
+ or name == ".DS_Store"
13
+ or name.endswith(".swp")
14
+ or name.endswith("~")
15
+ )
16
+
17
+
18
+ def normalize_bundle_path(value: str) -> str | None:
19
+ if not value or "\\" in value or value.startswith("/") or value[1:2] == ":":
20
+ raise KitupError(f"invalid skill file path: {value}")
21
+
22
+ parts = value.split("/")
23
+ for part in parts:
24
+ if not part or part in {".", ".."}:
25
+ raise KitupError(f"invalid skill file path: {value}")
26
+ if skip_name(part):
27
+ return None
28
+
29
+ return "/".join(parts)
30
+
31
+
32
+ def resolve_path(path: str, *, cwd: str | None = None) -> Path:
33
+ resolved = Path(path)
34
+ if not resolved.is_absolute() and cwd is not None:
35
+ resolved = Path(cwd) / resolved
36
+ return resolved
37
+
38
+
39
+ def trim_github_path(path: str) -> str:
40
+ return path.strip("/")
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from ._github import fetch_github_directory
10
+ from ._paths import normalize_bundle_path, resolve_path, skip_name
11
+ from .types import (
12
+ BundleFile,
13
+ GitHubBundleOptions,
14
+ KitupError,
15
+ NormalizedSkillBundle,
16
+ SkillFile,
17
+ SkillInfo,
18
+ )
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class DirectoryBundle:
23
+ path: str
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FilesBundle:
28
+ files: list[SkillFile]
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class GitHubBundle:
33
+ options: GitHubBundleOptions
34
+
35
+
36
+ SkillBundle = DirectoryBundle | FilesBundle | GitHubBundle
37
+
38
+
39
+ def directory_bundle(path: str) -> DirectoryBundle:
40
+ return DirectoryBundle(path=path)
41
+
42
+
43
+ def files_bundle(files: list[SkillFile]) -> FilesBundle:
44
+ return FilesBundle(files=files)
45
+
46
+
47
+ def github_bundle(options: GitHubBundleOptions) -> GitHubBundle:
48
+ return GitHubBundle(options=options)
49
+
50
+
51
+ def validate_skill_bundle(bundle: SkillBundle, cwd: str | None = None) -> SkillInfo:
52
+ try:
53
+ normalized = normalize_skill_bundle(bundle, cwd=cwd)
54
+ except Exception:
55
+ return SkillInfo(valid=False, error_code="invalid-skill-bundle")
56
+
57
+ return validate_normalized_skill_bundle(normalized)
58
+
59
+
60
+ def validate_normalized_skill_bundle(normalized: NormalizedSkillBundle) -> SkillInfo:
61
+ skill_md = normalized.by_path.get("SKILL.md")
62
+ if skill_md is None:
63
+ return SkillInfo(valid=False, error_code="missing-skill-md")
64
+
65
+ try:
66
+ content = skill_md.bytes.decode("utf-8")
67
+ except UnicodeDecodeError:
68
+ return SkillInfo(valid=False, error_code="invalid-frontmatter")
69
+
70
+ match = re.match(r"^---\r?\n([\s\S]*?)\r?\n---\r?\n", content)
71
+ if match is None:
72
+ return SkillInfo(valid=False, error_code="invalid-frontmatter")
73
+
74
+ fields = _parse_frontmatter(match.group(1))
75
+ skill_name = fields.get("name", "")
76
+ description = fields.get("description", "")
77
+ if not _valid_skill_name(skill_name) or not description or len(description) > 1024:
78
+ return SkillInfo(valid=False, error_code="invalid-frontmatter")
79
+
80
+ return SkillInfo(valid=True, skill_name=skill_name, description=description)
81
+
82
+
83
+ def compute_bundle_content_hash(bundle: SkillBundle, cwd: str | None = None) -> str:
84
+ normalized = normalize_skill_bundle(bundle, cwd=cwd)
85
+ return compute_normalized_bundle_content_hash(normalized)
86
+
87
+
88
+ def compute_normalized_bundle_content_hash(normalized: NormalizedSkillBundle) -> str:
89
+ digest = hashlib.sha256()
90
+ for file in normalized.files:
91
+ digest.update(file.path.encode("utf-8"))
92
+ digest.update(b"\0")
93
+ digest.update(file.bytes)
94
+ digest.update(b"\0")
95
+ return f"sha256:{digest.hexdigest()}"
96
+
97
+
98
+ def normalize_skill_bundle(
99
+ bundle: SkillBundle, cwd: str | None = None
100
+ ) -> NormalizedSkillBundle:
101
+ if isinstance(bundle, DirectoryBundle):
102
+ return normalize_directory_bundle(bundle.path, cwd=cwd)
103
+ if isinstance(bundle, FilesBundle):
104
+ return normalize_files_bundle(bundle.files)
105
+ if isinstance(bundle, GitHubBundle):
106
+ return normalize_files_bundle(fetch_github_directory(bundle.options))
107
+ raise KitupError(f"unsupported bundle: {type(bundle)!r}")
108
+
109
+
110
+ def normalize_directory_bundle(
111
+ path: str, cwd: str | None = None
112
+ ) -> NormalizedSkillBundle:
113
+ root = resolve_path(path, cwd=cwd)
114
+ if not root.is_dir():
115
+ raise KitupError(f"invalid bundle directory: {root}")
116
+ files: list[SkillFile] = []
117
+
118
+ for current_root, dirnames, filenames in os.walk(root):
119
+ dirnames[:] = sorted(name for name in dirnames if not skip_name(name))
120
+ for filename in sorted(filenames):
121
+ if skip_name(filename):
122
+ continue
123
+ source = Path(current_root) / filename
124
+ relative = source.relative_to(root).as_posix()
125
+ files.append(
126
+ SkillFile(
127
+ path=relative,
128
+ contents=source.read_bytes(),
129
+ mode=source.stat().st_mode & 0o777,
130
+ )
131
+ )
132
+
133
+ return normalize_files_bundle(files)
134
+
135
+
136
+ def normalize_files_bundle(files: list[SkillFile]) -> NormalizedSkillBundle:
137
+ by_path: dict[str, BundleFile] = {}
138
+ for file in files:
139
+ normalized_path = normalize_bundle_path(file.path)
140
+ if normalized_path is None:
141
+ continue
142
+ if normalized_path in by_path:
143
+ raise KitupError(f"duplicate skill file: {normalized_path}")
144
+ by_path[normalized_path] = BundleFile(
145
+ path=normalized_path,
146
+ bytes=file.contents.encode("utf-8")
147
+ if isinstance(file.contents, str)
148
+ else file.contents,
149
+ mode=file.mode or 0o644,
150
+ )
151
+
152
+ normalized_files = [by_path[path] for path in sorted(by_path)]
153
+ return NormalizedSkillBundle(files=normalized_files, by_path=by_path)
154
+
155
+
156
+ def copy_normalized_bundle(files: list[BundleFile], target_dir: str | Path) -> None:
157
+ destination_root = Path(target_dir)
158
+ destination_root.mkdir(parents=True, exist_ok=True)
159
+ for file in files:
160
+ destination = destination_root / file.path
161
+ destination.parent.mkdir(parents=True, exist_ok=True)
162
+ destination.write_bytes(file.bytes)
163
+ destination.chmod(file.mode)
164
+
165
+
166
+ def _parse_frontmatter(content: str) -> dict[str, str]:
167
+ fields: dict[str, str] = {}
168
+ for line in content.splitlines():
169
+ if ":" not in line:
170
+ continue
171
+ key, value = line.split(":", 1)
172
+ fields[key] = value.strip()
173
+ return fields
174
+
175
+
176
+ def _valid_skill_name(name: str) -> bool:
177
+ return re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) is not None