funchub-sdk 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,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: funchub-sdk
3
+ Version: 0.1.0
4
+ Summary: FuncHub - A tool registry and dynamic loader for AI agents
5
+ Author: FuncHub Team
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: click>=8.0
10
+ Requires-Dist: requests>=2.28
11
+ Requires-Dist: packaging>=23.0
12
+ Requires-Dist: PyYAML>=6.0
13
+ Requires-Dist: pydantic>=2.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
17
+ Requires-Dist: pytest-mock>=3.0; extra == "dev"
18
+ Requires-Dist: mypy>=1.0; extra == "dev"
19
+ Requires-Dist: black>=23.0; extra == "dev"
20
+ Requires-Dist: ruff>=0.1; extra == "dev"
21
+ Requires-Dist: responses>=0.25; extra == "dev"
22
+
23
+ # FuncHub
24
+
25
+ > ⚠️ **Security Warning**: FuncHub dynamically loads and executes code from remote Git repositories. **Only install tools from sources you trust**, and use in isolated environments.
26
+
27
+ FuncHub is a bilingual (Python + NestJS) tool registry and dynamic loader designed for AI Agents. It allows developers to publish, discover, install, and dynamically invoke tool functions.
28
+
29
+ ## Features
30
+
31
+ - **Dual SDKs**: Python SDK + NestJS SDK, sharing the same design philosophy
32
+ - **Semver version management**: Supports `^1.2.3`, `1.x`, `latest`, branch names, etc.
33
+ - **GitHub publishing integration**: Automatic Fork + PR workflow via GitHub API v3
34
+ - **Cache management**: `.version` file tracks installed versions; expired cache auto-refetches
35
+ - **Private registry support**: Supports custom indexes and mirrors via config
36
+ - **Safety confirmation**: Interactive confirmation on install; `--yes` to bypass
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ### Python
43
+
44
+ ```bash
45
+ pip install funchub-sdk
46
+ ```
47
+
48
+ ### NestJS
49
+
50
+ ```bash
51
+ npm install funchub-nestjs
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Quick Start
57
+
58
+ ### 1. Configure GitHub Token
59
+
60
+ ```bash
61
+ # Python
62
+ funchub login --token ghp_xxxxxxxxxxxx
63
+
64
+ # NestJS
65
+ npx funchub login --token ghp_xxxxxxxxxxxx
66
+ ```
67
+
68
+ ### 2. Search for Tools
69
+
70
+ ```bash
71
+ funchub search scraper
72
+ funchub search web
73
+ ```
74
+
75
+ ### 3. Install a Tool
76
+
77
+ ```bash
78
+ # Install latest version
79
+ funchub install web_scraper
80
+
81
+ # Install with version constraint
82
+ funchub install web_scraper@^1.0
83
+
84
+ # Install development branch
85
+ funchub install web_scraper@main
86
+ ```
87
+
88
+ ### 4. List Installed Tools
89
+
90
+ ```bash
91
+ funchub list
92
+ ```
93
+
94
+ ### 5. Use a Tool in Code
95
+
96
+ **Python:**
97
+ ```python
98
+ from funchub import FuncHub
99
+
100
+ hub = FuncHub()
101
+ scraper = hub.load("web_scraper")
102
+ result = scraper(url="https://example.com")
103
+ ```
104
+
105
+ **NestJS:**
106
+ ```typescript
107
+ import { FuncHub } from '@funchub/nestjs';
108
+
109
+ const hub = new FuncHub();
110
+ const scraper = await hub.load('web_scraper');
111
+ const result = await scraper({ url: 'https://example.com' });
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Command Reference
117
+
118
+ | Command | Description |
119
+ |---------|-------------|
120
+ | `funchub login --token <PAT>` | Save GitHub Personal Access Token |
121
+ | `funchub config set <key> <value>` | Set configuration (e.g., custom registry) |
122
+ | `funchub publish --version v1.0.0` | Publish current directory as a tool |
123
+ | `funchub publish --version v1.0.0 --force` | Overwrite existing tool with same name |
124
+ | `funchub publish --version v1.0.0 --dry-run` | Preview without actual submission |
125
+ | `funchub search <query>` | Search for tools in the registry |
126
+ | `funchub install <name>@<constraint>` | Install a tool |
127
+ | `funchub list` | List locally installed tools |
128
+ | `funchub update <name>` | Update a tool to latest version |
129
+ | `funchub update --all` | Update all installed tools |
130
+ | `funchub info <name>` | Show tool details |
131
+ | `funchub uninstall <name>` | Remove local tool cache |
132
+
133
+ ---
134
+
135
+ ## Publishing a Tool
136
+
137
+ 1. Create a `funchub.json` or `funchub.yaml` tool definition file in your project root:
138
+
139
+ ```json
140
+ {
141
+ "name": "my_tool",
142
+ "description": "My awesome tool",
143
+ "version": "1.0.0",
144
+ "entry_point": "src/index:handler",
145
+ "parameters": {
146
+ "type": "object",
147
+ "properties": {
148
+ "input": { "type": "string" }
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ 2. Publish:
155
+
156
+ ```bash
157
+ funchub publish --version v1.0.0
158
+ ```
159
+
160
+ If you have write access to the registry repo, the tool is committed directly. Otherwise, a Fork + PR is automatically created.
161
+
162
+ ---
163
+
164
+ ## Decorator Usage (Python)
165
+
166
+ ```python
167
+ from funchub import funchub_tool
168
+
169
+ @funchub_tool(
170
+ name="web_scraper",
171
+ description="Scrape web page title",
172
+ version="1.0.0"
173
+ )
174
+ def scrape_url(url: str) -> str:
175
+ """Scrape the title from a URL."""
176
+ import requests
177
+ from bs4 import BeautifulSoup
178
+ resp = requests.get(url)
179
+ soup = BeautifulSoup(resp.text, 'html.parser')
180
+ return soup.title.string if soup.title else "No title found"
181
+ ```
182
+
183
+ ---
184
+
185
+ ## License
186
+
187
+ Apache License 2.0
@@ -0,0 +1,165 @@
1
+ # FuncHub
2
+
3
+ > ⚠️ **Security Warning**: FuncHub dynamically loads and executes code from remote Git repositories. **Only install tools from sources you trust**, and use in isolated environments.
4
+
5
+ FuncHub is a bilingual (Python + NestJS) tool registry and dynamic loader designed for AI Agents. It allows developers to publish, discover, install, and dynamically invoke tool functions.
6
+
7
+ ## Features
8
+
9
+ - **Dual SDKs**: Python SDK + NestJS SDK, sharing the same design philosophy
10
+ - **Semver version management**: Supports `^1.2.3`, `1.x`, `latest`, branch names, etc.
11
+ - **GitHub publishing integration**: Automatic Fork + PR workflow via GitHub API v3
12
+ - **Cache management**: `.version` file tracks installed versions; expired cache auto-refetches
13
+ - **Private registry support**: Supports custom indexes and mirrors via config
14
+ - **Safety confirmation**: Interactive confirmation on install; `--yes` to bypass
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ ### Python
21
+
22
+ ```bash
23
+ pip install funchub-sdk
24
+ ```
25
+
26
+ ### NestJS
27
+
28
+ ```bash
29
+ npm install funchub-nestjs
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Configure GitHub Token
37
+
38
+ ```bash
39
+ # Python
40
+ funchub login --token ghp_xxxxxxxxxxxx
41
+
42
+ # NestJS
43
+ npx funchub login --token ghp_xxxxxxxxxxxx
44
+ ```
45
+
46
+ ### 2. Search for Tools
47
+
48
+ ```bash
49
+ funchub search scraper
50
+ funchub search web
51
+ ```
52
+
53
+ ### 3. Install a Tool
54
+
55
+ ```bash
56
+ # Install latest version
57
+ funchub install web_scraper
58
+
59
+ # Install with version constraint
60
+ funchub install web_scraper@^1.0
61
+
62
+ # Install development branch
63
+ funchub install web_scraper@main
64
+ ```
65
+
66
+ ### 4. List Installed Tools
67
+
68
+ ```bash
69
+ funchub list
70
+ ```
71
+
72
+ ### 5. Use a Tool in Code
73
+
74
+ **Python:**
75
+ ```python
76
+ from funchub import FuncHub
77
+
78
+ hub = FuncHub()
79
+ scraper = hub.load("web_scraper")
80
+ result = scraper(url="https://example.com")
81
+ ```
82
+
83
+ **NestJS:**
84
+ ```typescript
85
+ import { FuncHub } from '@funchub/nestjs';
86
+
87
+ const hub = new FuncHub();
88
+ const scraper = await hub.load('web_scraper');
89
+ const result = await scraper({ url: 'https://example.com' });
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Command Reference
95
+
96
+ | Command | Description |
97
+ |---------|-------------|
98
+ | `funchub login --token <PAT>` | Save GitHub Personal Access Token |
99
+ | `funchub config set <key> <value>` | Set configuration (e.g., custom registry) |
100
+ | `funchub publish --version v1.0.0` | Publish current directory as a tool |
101
+ | `funchub publish --version v1.0.0 --force` | Overwrite existing tool with same name |
102
+ | `funchub publish --version v1.0.0 --dry-run` | Preview without actual submission |
103
+ | `funchub search <query>` | Search for tools in the registry |
104
+ | `funchub install <name>@<constraint>` | Install a tool |
105
+ | `funchub list` | List locally installed tools |
106
+ | `funchub update <name>` | Update a tool to latest version |
107
+ | `funchub update --all` | Update all installed tools |
108
+ | `funchub info <name>` | Show tool details |
109
+ | `funchub uninstall <name>` | Remove local tool cache |
110
+
111
+ ---
112
+
113
+ ## Publishing a Tool
114
+
115
+ 1. Create a `funchub.json` or `funchub.yaml` tool definition file in your project root:
116
+
117
+ ```json
118
+ {
119
+ "name": "my_tool",
120
+ "description": "My awesome tool",
121
+ "version": "1.0.0",
122
+ "entry_point": "src/index:handler",
123
+ "parameters": {
124
+ "type": "object",
125
+ "properties": {
126
+ "input": { "type": "string" }
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ 2. Publish:
133
+
134
+ ```bash
135
+ funchub publish --version v1.0.0
136
+ ```
137
+
138
+ If you have write access to the registry repo, the tool is committed directly. Otherwise, a Fork + PR is automatically created.
139
+
140
+ ---
141
+
142
+ ## Decorator Usage (Python)
143
+
144
+ ```python
145
+ from funchub import funchub_tool
146
+
147
+ @funchub_tool(
148
+ name="web_scraper",
149
+ description="Scrape web page title",
150
+ version="1.0.0"
151
+ )
152
+ def scrape_url(url: str) -> str:
153
+ """Scrape the title from a URL."""
154
+ import requests
155
+ from bs4 import BeautifulSoup
156
+ resp = requests.get(url)
157
+ soup = BeautifulSoup(resp.text, 'html.parser')
158
+ return soup.title.string if soup.title else "No title found"
159
+ ```
160
+
161
+ ---
162
+
163
+ ## License
164
+
165
+ Apache License 2.0
@@ -0,0 +1,3 @@
1
+ from funchub.client import FuncHub
2
+
3
+ __all__ = ["FuncHub"]
@@ -0,0 +1,204 @@
1
+ import sys
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import click
6
+ import yaml
7
+
8
+ from funchub.client import FuncHub
9
+ from funchub.exceptions import ConflictError, FuncHubError, ToolNotFoundError, VersionNotFoundError
10
+ from funchub.github_client import load_config, save_config
11
+ from funchub.models import ToolDefinition, ToolVersion
12
+
13
+
14
+ @click.group()
15
+ @click.option("--registry", "-r", envvar="FUNCHUB_REGISTRY", help="中央索引 URL")
16
+ @click.option("--token", envvar="GITHUB_TOKEN", help="GitHub PAT")
17
+ @click.pass_context
18
+ def cli(ctx: click.Context, registry: Optional[str], token: Optional[str]) -> None:
19
+ ctx.ensure_object(dict)
20
+ ctx.obj["hub"] = FuncHub(registry=registry, token=token)
21
+
22
+
23
+ @cli.command()
24
+ @click.option("--token", required=True, help="GitHub Personal Access Token")
25
+ def login(token: str) -> None:
26
+ cfg = load_config()
27
+ cfg["github_token"] = token
28
+ save_config(cfg)
29
+ click.echo("GitHub Token 已保存到 ~/.funchub/config.yaml")
30
+
31
+
32
+ @cli.command()
33
+ @click.argument("key")
34
+ @click.argument("value")
35
+ def config(key: str, value: str) -> None:
36
+ cfg = load_config()
37
+ cfg[key] = value
38
+ save_config(cfg)
39
+ click.echo(f"配置项 {key} 已设置为 {value}")
40
+
41
+
42
+ @cli.command()
43
+ @click.option("--version", "ver", required=True, help="发布的版本号")
44
+ @click.option("--force", is_flag=True, help="覆盖同名工具")
45
+ @click.option("--dry-run", is_flag=True, help="预览不实际提交")
46
+ @click.pass_context
47
+ def publish(ctx: click.Context, ver: str, force: bool, dry_run: bool) -> None:
48
+ hub: FuncHub = ctx.obj["hub"]
49
+ tool_file = Path("funchub-tool.yaml")
50
+ if not tool_file.exists():
51
+ click.echo("错误: 当前目录未找到 funchub-tool.yaml", err=True)
52
+ sys.exit(1)
53
+ raw = tool_file.read_text(encoding="utf-8")
54
+ data = yaml.safe_load(raw)
55
+ is_prerelease = any(tag in ver.lower() for tag in ("alpha", "beta", "rc", "pre"))
56
+ tv = ToolVersion(
57
+ version=ver,
58
+ source_repo=data.get("source_repo", ""),
59
+ source_ref=data.get("source_ref", f"v{ver}"),
60
+ dependencies=data.get("dependencies", []),
61
+ is_prerelease=is_prerelease,
62
+ )
63
+ tool_def = ToolDefinition(
64
+ name=data["name"],
65
+ description=data.get("description", ""),
66
+ parameters=data.get("parameters", {"type": "object", "properties": {}}),
67
+ author=data.get("author", "anonymous"),
68
+ entry_point=data.get("entry_point", "index:main"),
69
+ versions=[tv],
70
+ )
71
+ try:
72
+ result = hub.publish(tool_def, force=force, dry_run=dry_run)
73
+ if dry_run:
74
+ click.echo(f"[DRY RUN] {result}")
75
+ else:
76
+ click.echo(f"发布成功: {result}")
77
+ except ConflictError as e:
78
+ click.echo(f"错误: {e}", err=True)
79
+ sys.exit(1)
80
+ except FuncHubError as e:
81
+ click.echo(f"错误: {e}", err=True)
82
+ sys.exit(1)
83
+
84
+
85
+ @cli.command()
86
+ @click.argument("query")
87
+ @click.pass_context
88
+ def search(ctx: click.Context, query: str) -> None:
89
+ hub: FuncHub = ctx.obj["hub"]
90
+ results = hub.search(query)
91
+ if not results:
92
+ click.echo("未找到匹配的工具")
93
+ return
94
+ for t in results:
95
+ click.echo(f"{t.name} - {t.description}")
96
+
97
+
98
+ @cli.command()
99
+ @click.argument("tool_spec")
100
+ @click.option("--prerelease", is_flag=True, help="包含预发布版本")
101
+ @click.option("--yes", is_flag=True, help="跳过安全确认")
102
+ @click.pass_context
103
+ def install(ctx: click.Context, tool_spec: str, prerelease: bool, yes: bool) -> None:
104
+ hub: FuncHub = ctx.obj["hub"]
105
+ constraint: Optional[str] = None
106
+ tool_name = tool_spec
107
+ if "@" in tool_spec:
108
+ tool_name, constraint = tool_spec.split("@", 1)
109
+ try:
110
+ hub.install(
111
+ tool_name,
112
+ constraint=constraint,
113
+ include_prerelease=prerelease,
114
+ yes=yes,
115
+ )
116
+ click.echo(f"✅ 工具 {tool_name} 安装成功")
117
+ except (ToolNotFoundError, VersionNotFoundError) as e:
118
+ click.echo(f"错误: {e}", err=True)
119
+ sys.exit(1)
120
+ except FuncHubError as e:
121
+ click.echo(f"错误: {e}", err=True)
122
+ sys.exit(1)
123
+
124
+
125
+ @cli.command("list")
126
+ @click.pass_context
127
+ def list_tools(ctx: click.Context) -> None:
128
+ hub: FuncHub = ctx.obj["hub"]
129
+ items = hub.list_installed()
130
+ if not items:
131
+ click.echo("未安装任何工具")
132
+ return
133
+ for item in items:
134
+ click.echo(f"{item['name']}@{item['version']} ({item['source_repo']})")
135
+
136
+
137
+ @cli.command()
138
+ @click.argument("name", required=False)
139
+ @click.option("--all", "update_all", is_flag=True, help="更新所有工具")
140
+ @click.option("--prerelease", is_flag=True, help="包含预发布版本")
141
+ @click.option("--yes", is_flag=True, help="跳过安全确认")
142
+ @click.pass_context
143
+ def update(
144
+ ctx: click.Context,
145
+ name: Optional[str],
146
+ update_all: bool,
147
+ prerelease: bool,
148
+ yes: bool,
149
+ ) -> None:
150
+ hub: FuncHub = ctx.obj["hub"]
151
+ if update_all:
152
+ results = hub.update_all(include_prerelease=prerelease, yes=yes)
153
+ if not results:
154
+ click.echo("所有工具已是最新")
155
+ for r in results:
156
+ click.echo(r)
157
+ return
158
+ if not name:
159
+ click.echo("请指定工具名称或使用 --all", err=True)
160
+ sys.exit(1)
161
+ try:
162
+ result = hub.update(name, include_prerelease=prerelease, yes=yes)
163
+ click.echo(f"✅ 工具 {name} 已更新到 {result}")
164
+ except FuncHubError as e:
165
+ click.echo(f"错误: {e}", err=True)
166
+ sys.exit(1)
167
+
168
+
169
+ @cli.command()
170
+ @click.argument("name")
171
+ @click.pass_context
172
+ def info(ctx: click.Context, name: str) -> None:
173
+ hub: FuncHub = ctx.obj["hub"]
174
+ tool_def = hub.info(name)
175
+ if tool_def is None:
176
+ click.echo(f"工具 '{name}' 未找到")
177
+ return
178
+ click.echo(f"名称: {tool_def.name}")
179
+ click.echo(f"描述: {tool_def.description}")
180
+ click.echo(f"作者: {tool_def.author}")
181
+ click.echo(f"入口: {tool_def.entry_point}")
182
+ click.echo("版本:")
183
+ for v in tool_def.versions:
184
+ pre = " (预发布)" if v.is_prerelease else ""
185
+ click.echo(f" - {v.version}{pre}")
186
+
187
+
188
+ @cli.command()
189
+ @click.argument("name")
190
+ @click.pass_context
191
+ def uninstall(ctx: click.Context, name: str) -> None:
192
+ hub: FuncHub = ctx.obj["hub"]
193
+ if hub.uninstall(name):
194
+ click.echo(f"✅ 工具 {name} 已卸载")
195
+ else:
196
+ click.echo(f"工具 {name} 未安装")
197
+
198
+
199
+ def main() -> None:
200
+ cli()
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()