amap-cli 0.1.0__tar.gz → 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.
Files changed (24) hide show
  1. {amap_cli-0.1.0 → amap_cli-0.1.2}/PKG-INFO +38 -2
  2. {amap_cli-0.1.0 → amap_cli-0.1.2}/README.md +37 -1
  3. {amap_cli-0.1.0 → amap_cli-0.1.2}/pyproject.toml +4 -1
  4. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/cli.py +2 -0
  5. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/config.py +69 -11
  6. amap_cli-0.1.2/src/amap_cli/skill.py +102 -0
  7. {amap_cli-0.1.0 → amap_cli-0.1.2}/uv.lock +1 -1
  8. {amap_cli-0.1.0 → amap_cli-0.1.2}/.gitignore +0 -0
  9. {amap_cli-0.1.0 → amap_cli-0.1.2}/.trae/specs/build-amap-cli/checklist.md +0 -0
  10. {amap_cli-0.1.0 → amap_cli-0.1.2}/.trae/specs/build-amap-cli/spec.md +0 -0
  11. {amap_cli-0.1.0 → amap_cli-0.1.2}/.trae/specs/build-amap-cli/tasks.md +0 -0
  12. {amap_cli-0.1.0 → amap_cli-0.1.2}/amap-gui-reference.md +0 -0
  13. {amap_cli-0.1.0 → amap_cli-0.1.2}/docs/manual-verification.md +0 -0
  14. {amap_cli-0.1.0 → amap_cli-0.1.2}/skills/amap-cli/SKILL.md +0 -0
  15. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/__init__.py +0 -0
  16. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/__main__.py +0 -0
  17. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/api.py +0 -0
  18. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/distance.py +0 -0
  19. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/errors.py +0 -0
  20. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/geocode.py +0 -0
  21. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/output.py +0 -0
  22. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/params.py +0 -0
  23. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/route.py +0 -0
  24. {amap_cli-0.1.0 → amap_cli-0.1.2}/src/amap_cli/search_poi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: amap-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Pure command-line CLI for Amap APIs.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -94,6 +94,24 @@ amap-cli search-poi --keyword 星巴克 --city 北京
94
94
  - `distance` 命令在起终点都为坐标时可直接本地计算,不依赖 API Key
95
95
  - 建议 Agent 直接解析命令返回的 JSON 结果
96
96
 
97
+ 如果需要将内置 skill 安装到指定目录,例如 Agent 的 skills 根目录,可以执行:
98
+
99
+ ```bash
100
+ amap-cli install-skill --dir ~/.agents/skills
101
+ ```
102
+
103
+ 执行后会在目标目录下生成:
104
+
105
+ ```text
106
+ ~/.agents/skills/amap-cli/SKILL.md
107
+ ```
108
+
109
+ 如果目标目录中已存在同名 skill,可追加 `--force` 覆盖:
110
+
111
+ ```bash
112
+ amap-cli install-skill --dir ~/.agents/skills --force
113
+ ```
114
+
97
115
  ## 命令说明
98
116
 
99
117
  ### 配置
@@ -116,6 +134,24 @@ amap-cli config show
116
134
  ~/Library/Application Support/amap-cli/config.json
117
135
  ```
118
136
 
137
+ ### 安装 Skill
138
+
139
+ 基础格式:
140
+
141
+ ```bash
142
+ amap-cli install-skill --dir <skills目录>
143
+ ```
144
+
145
+ 可选参数:
146
+
147
+ - `--force`
148
+
149
+ 说明:
150
+
151
+ - 会将内置的 `amap-cli` skill 安装到目标目录下的 `amap-cli/` 子目录
152
+ - 当目标目录不存在时会自动创建
153
+ - 当目标目录中已存在同名 skill 时,默认报错;追加 `--force` 后会覆盖
154
+
119
155
  ### 路径规划
120
156
 
121
157
  基础格式:
@@ -268,7 +304,7 @@ uv publish \
268
304
  如果只是验证包能否被安装,可以执行:
269
305
 
270
306
  ```bash
271
- uvx --index-url https://test.pypi.org/simple/ amap-cli --help
307
+ uvx --index https://test.pypi.org/simple/ amap-cli --help
272
308
  ```
273
309
 
274
310
  ### 5. 发布到正式 PyPI
@@ -87,6 +87,24 @@ amap-cli search-poi --keyword 星巴克 --city 北京
87
87
  - `distance` 命令在起终点都为坐标时可直接本地计算,不依赖 API Key
88
88
  - 建议 Agent 直接解析命令返回的 JSON 结果
89
89
 
90
+ 如果需要将内置 skill 安装到指定目录,例如 Agent 的 skills 根目录,可以执行:
91
+
92
+ ```bash
93
+ amap-cli install-skill --dir ~/.agents/skills
94
+ ```
95
+
96
+ 执行后会在目标目录下生成:
97
+
98
+ ```text
99
+ ~/.agents/skills/amap-cli/SKILL.md
100
+ ```
101
+
102
+ 如果目标目录中已存在同名 skill,可追加 `--force` 覆盖:
103
+
104
+ ```bash
105
+ amap-cli install-skill --dir ~/.agents/skills --force
106
+ ```
107
+
90
108
  ## 命令说明
91
109
 
92
110
  ### 配置
@@ -109,6 +127,24 @@ amap-cli config show
109
127
  ~/Library/Application Support/amap-cli/config.json
110
128
  ```
111
129
 
130
+ ### 安装 Skill
131
+
132
+ 基础格式:
133
+
134
+ ```bash
135
+ amap-cli install-skill --dir <skills目录>
136
+ ```
137
+
138
+ 可选参数:
139
+
140
+ - `--force`
141
+
142
+ 说明:
143
+
144
+ - 会将内置的 `amap-cli` skill 安装到目标目录下的 `amap-cli/` 子目录
145
+ - 当目标目录不存在时会自动创建
146
+ - 当目标目录中已存在同名 skill 时,默认报错;追加 `--force` 后会覆盖
147
+
112
148
  ### 路径规划
113
149
 
114
150
  基础格式:
@@ -261,7 +297,7 @@ uv publish \
261
297
  如果只是验证包能否被安装,可以执行:
262
298
 
263
299
  ```bash
264
- uvx --index-url https://test.pypi.org/simple/ amap-cli --help
300
+ uvx --index https://test.pypi.org/simple/ amap-cli --help
265
301
  ```
266
302
 
267
303
  ### 5. 发布到正式 PyPI
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "amap-cli"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Pure command-line CLI for Amap APIs."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -16,5 +16,8 @@ amap-cli = "amap_cli.cli:main"
16
16
  [tool.hatch.build.targets.wheel]
17
17
  packages = ["src/amap_cli"]
18
18
 
19
+ [tool.hatch.build.targets.wheel.force-include]
20
+ "skills" = "amap_cli/skills"
21
+
19
22
  [tool.uv]
20
23
  package = true
@@ -11,6 +11,7 @@ from amap_cli.errors import AmapCliError, ValidationError
11
11
  from amap_cli.output import emit_error, emit_success
12
12
  from amap_cli.route import register_route_command
13
13
  from amap_cli.search_poi import register_search_poi_command
14
+ from amap_cli.skill import register_install_skill_command
14
15
 
15
16
 
16
17
  class JsonArgumentParser(argparse.ArgumentParser):
@@ -29,6 +30,7 @@ def build_parser() -> JsonArgumentParser:
29
30
  subparsers = parser.add_subparsers(dest="command")
30
31
 
31
32
  _register_config_command(subparsers)
33
+ register_install_skill_command(subparsers)
32
34
  register_distance_command(subparsers)
33
35
  register_route_command(subparsers)
34
36
  register_search_poi_command(subparsers)
@@ -81,27 +81,67 @@ def get_config_path() -> Path:
81
81
  return get_config_dir() / CONFIG_FILE_NAME
82
82
 
83
83
 
84
+ def _merge_error_details(
85
+ current: Any | None,
86
+ *,
87
+ path: Path,
88
+ operation: str | None = None,
89
+ exc: BaseException | None = None,
90
+ ) -> dict[str, Any]:
91
+ """Attach config file context to an error payload."""
92
+ details = current.copy() if isinstance(current, dict) else {}
93
+ details["path"] = str(path)
94
+ if operation is not None:
95
+ details["operation"] = operation
96
+ if exc is not None:
97
+ details["type"] = type(exc).__name__
98
+ details["reason"] = str(exc)
99
+ return details
100
+
101
+
84
102
  def load_config(*, required: bool = False) -> AmapConfig | None:
85
103
  """Load config from disk, optionally requiring it to exist."""
86
104
  path = get_config_path()
87
- if not path.exists():
105
+ try:
106
+ exists = path.exists()
107
+ except OSError as exc:
108
+ raise ConfigError(
109
+ "检查配置文件状态失败。",
110
+ details=_merge_error_details(None, path=path, operation="stat", exc=exc),
111
+ ) from exc
112
+
113
+ if not exists:
88
114
  if required:
89
115
  raise MissingConfigError(
90
- "未找到高德 API 配置,请先执行 `amap-cli config set --api-key <KEY>`。"
116
+ "未找到高德 API 配置,请先执行 `amap-cli config set --api-key <KEY>`。",
117
+ details=_merge_error_details(None, path=path),
91
118
  )
92
119
  return None
93
120
 
94
121
  try:
95
122
  raw = json.loads(path.read_text(encoding="utf-8"))
96
123
  except json.JSONDecodeError as exc:
97
- raise ConfigError("配置文件不是合法的 JSON。", details={"path": str(path)}) from exc
124
+ raise ConfigError(
125
+ "配置文件不是合法的 JSON。",
126
+ details=_merge_error_details(None, path=path, operation="read", exc=exc),
127
+ ) from exc
98
128
  except OSError as exc:
99
- raise ConfigError("读取配置文件失败。", details={"path": str(path)}) from exc
129
+ raise ConfigError(
130
+ "读取配置文件失败。",
131
+ details=_merge_error_details(None, path=path, operation="read", exc=exc),
132
+ ) from exc
100
133
 
101
134
  if not isinstance(raw, dict):
102
- raise ConfigError("配置文件内容格式无效。", details={"path": str(path)})
135
+ raise ConfigError(
136
+ "配置文件内容格式无效。",
137
+ details=_merge_error_details(None, path=path),
138
+ )
103
139
 
104
- return AmapConfig.from_dict(raw)
140
+ try:
141
+ return AmapConfig.from_dict(raw)
142
+ except ConfigError as exc:
143
+ exc.details = _merge_error_details(exc.details, path=path)
144
+ raise
105
145
 
106
146
 
107
147
  def save_config(
@@ -131,11 +171,29 @@ def save_config(
131
171
  config.timeout_seconds = float(timeout_seconds)
132
172
 
133
173
  path = get_config_path()
134
- path.parent.mkdir(parents=True, exist_ok=True)
135
- path.write_text(
136
- json.dumps(config.to_dict(), ensure_ascii=False, indent=2) + "\n",
137
- encoding="utf-8",
138
- )
174
+ try:
175
+ path.parent.mkdir(parents=True, exist_ok=True)
176
+ except OSError as exc:
177
+ raise ConfigError(
178
+ "创建配置目录失败。",
179
+ details=_merge_error_details(
180
+ None,
181
+ path=path.parent,
182
+ operation="create_dir",
183
+ exc=exc,
184
+ ),
185
+ ) from exc
186
+
187
+ try:
188
+ path.write_text(
189
+ json.dumps(config.to_dict(), ensure_ascii=False, indent=2) + "\n",
190
+ encoding="utf-8",
191
+ )
192
+ except OSError as exc:
193
+ raise ConfigError(
194
+ "写入配置文件失败。",
195
+ details=_merge_error_details(None, path=path, operation="write", exc=exc),
196
+ ) from exc
139
197
 
140
198
  if os.name != "nt":
141
199
  try:
@@ -0,0 +1,102 @@
1
+ """Skill installation commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import shutil
7
+ from importlib import resources
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from amap_cli.errors import AmapCliError
12
+
13
+
14
+ def register_install_skill_command(
15
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
16
+ ) -> None:
17
+ """Register the bundled skill installation command."""
18
+ parser = subparsers.add_parser(
19
+ "install-skill",
20
+ help="安装内置 skill 到指定目录",
21
+ )
22
+ parser.add_argument(
23
+ "--dir",
24
+ dest="target_dir",
25
+ required=True,
26
+ help="skill 安装根目录,例如 ~/.agents/skills",
27
+ )
28
+ parser.add_argument(
29
+ "--force",
30
+ action="store_true",
31
+ help="目标 skill 已存在时覆盖",
32
+ )
33
+ parser.set_defaults(handler=_handle_install_skill)
34
+
35
+
36
+ def _handle_install_skill(args: argparse.Namespace) -> dict[str, Any]:
37
+ """Install the bundled skill into the requested directory."""
38
+ skill_name = "amap-cli"
39
+ source_dir = _resolve_bundled_skill_dir()
40
+ target_root = Path(args.target_dir).expanduser().resolve()
41
+ target_skill_dir = target_root / skill_name
42
+
43
+ overwritten = False
44
+ if target_skill_dir.exists():
45
+ if not args.force:
46
+ raise AmapCliError(
47
+ f"目标目录已存在 skill:{target_skill_dir}",
48
+ code="SKILL_ALREADY_EXISTS",
49
+ details={
50
+ "skill_path": str(target_skill_dir),
51
+ "hint": "如需覆盖,请追加 --force。",
52
+ },
53
+ exit_code=1,
54
+ )
55
+ if target_skill_dir.is_dir():
56
+ shutil.rmtree(target_skill_dir)
57
+ else:
58
+ target_skill_dir.unlink()
59
+ overwritten = True
60
+
61
+ target_root.mkdir(parents=True, exist_ok=True)
62
+ _copy_tree(source_dir, target_skill_dir)
63
+
64
+ return {
65
+ "message": "skill 安装完成。",
66
+ "skill_name": skill_name,
67
+ "source": str(source_dir),
68
+ "target_dir": str(target_root),
69
+ "skill_path": str(target_skill_dir),
70
+ "overwritten": overwritten,
71
+ }
72
+
73
+
74
+ def _resolve_bundled_skill_dir() -> Any:
75
+ """Locate the bundled skill directory in installed or source layouts."""
76
+ packaged_skill_dir = resources.files("amap_cli").joinpath("skills", "amap-cli")
77
+ packaged_skill_file = packaged_skill_dir.joinpath("SKILL.md")
78
+ if packaged_skill_file.is_file():
79
+ return packaged_skill_dir
80
+
81
+ repo_skill_dir = Path(__file__).resolve().parents[2] / "skills" / "amap-cli"
82
+ if repo_skill_dir.is_dir():
83
+ return repo_skill_dir
84
+
85
+ raise AmapCliError(
86
+ "未找到内置 skill 资源。",
87
+ code="SKILL_RESOURCE_NOT_FOUND",
88
+ details={"skill_name": "amap-cli"},
89
+ exit_code=1,
90
+ )
91
+
92
+
93
+ def _copy_tree(source_dir: Any, target_dir: Path) -> None:
94
+ """Recursively copy the skill directory into the target path."""
95
+ target_dir.mkdir(parents=True, exist_ok=True)
96
+ for source_path in source_dir.iterdir():
97
+ target_path = target_dir / source_path.name
98
+ if source_path.is_dir():
99
+ _copy_tree(source_path, target_path)
100
+ continue
101
+ with source_path.open("rb") as source_file, target_path.open("wb") as target_file:
102
+ shutil.copyfileobj(source_file, target_file)
@@ -4,5 +4,5 @@ requires-python = ">=3.11"
4
4
 
5
5
  [[package]]
6
6
  name = "amap-cli"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  source = { editable = "." }
File without changes
File without changes
File without changes
File without changes