aes-cli 0.4.0__tar.gz → 0.4.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 (76) hide show
  1. {aes_cli-0.4.0 → aes_cli-0.4.2}/PKG-INFO +4 -1
  2. {aes_cli-0.4.0 → aes_cli-0.4.2}/README.md +3 -0
  3. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/__init__.py +1 -1
  4. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/sync.py +21 -0
  5. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/i18n/_messages.py +1 -1
  6. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/i18n/ja.py +1 -1
  7. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/registry.py +22 -40
  8. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/targets/claude.py +4 -2
  9. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes_cli.egg-info/PKG-INFO +4 -1
  10. {aes_cli-0.4.0 → aes_cli-0.4.2}/pyproject.toml +1 -1
  11. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_registry.py +2 -2
  12. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/__main__.py +0 -0
  13. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/analyzer.py +0 -0
  14. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/__init__.py +0 -0
  15. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/init.py +0 -0
  16. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/inspect.py +0 -0
  17. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/install.py +0 -0
  18. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/publish.py +0 -0
  19. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/search.py +0 -0
  20. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/status.py +0 -0
  21. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/commands/validate.py +0 -0
  22. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/config.py +0 -0
  23. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/domains.py +0 -0
  24. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/frameworks.py +0 -0
  25. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/global_config.py +0 -0
  26. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/i18n/__init__.py +0 -0
  27. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/i18n/domains_ja.py +0 -0
  28. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/mcp_server.py +0 -0
  29. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/agent.yaml.jinja +0 -0
  30. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/agentignore.jinja +0 -0
  31. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/instructions.md.jinja +0 -0
  32. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/ja/instructions.md.jinja +0 -0
  33. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/ja/operations.md.jinja +0 -0
  34. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/ja/orchestrator.md.jinja +0 -0
  35. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/ja/setup.md.jinja +0 -0
  36. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/ja/skill.md.jinja +0 -0
  37. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/ja/workflow_command.md.jinja +0 -0
  38. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/local.example.yaml.jinja +0 -0
  39. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/local.yaml.jinja +0 -0
  40. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/operations.md.jinja +0 -0
  41. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/orchestrator.md.jinja +0 -0
  42. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/permissions.yaml.jinja +0 -0
  43. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/setup.md.jinja +0 -0
  44. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/skill.md.jinja +0 -0
  45. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/skill.yaml.jinja +0 -0
  46. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/workflow.yaml.jinja +0 -0
  47. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/scaffold/workflow_command.md.jinja +0 -0
  48. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/schemas/agent.schema.json +0 -0
  49. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/schemas/permissions.schema.json +0 -0
  50. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/schemas/registry.schema.json +0 -0
  51. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/schemas/skill.schema.json +0 -0
  52. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/schemas/workflow.schema.json +0 -0
  53. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/targets/__init__.py +0 -0
  54. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/targets/_base.py +0 -0
  55. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/targets/_composer.py +0 -0
  56. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/targets/copilot.py +0 -0
  57. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/targets/cursor.py +0 -0
  58. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/targets/windsurf.py +0 -0
  59. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes/validator.py +0 -0
  60. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes_cli.egg-info/SOURCES.txt +0 -0
  61. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes_cli.egg-info/dependency_links.txt +0 -0
  62. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes_cli.egg-info/entry_points.txt +0 -0
  63. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes_cli.egg-info/requires.txt +0 -0
  64. {aes_cli-0.4.0 → aes_cli-0.4.2}/aes_cli.egg-info/top_level.txt +0 -0
  65. {aes_cli-0.4.0 → aes_cli-0.4.2}/setup.cfg +0 -0
  66. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_analyzer.py +0 -0
  67. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_frameworks.py +0 -0
  68. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_init.py +0 -0
  69. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_inspect.py +0 -0
  70. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_install.py +0 -0
  71. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_mcp_server.py +0 -0
  72. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_publish.py +0 -0
  73. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_search.py +0 -0
  74. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_status.py +0 -0
  75. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_sync.py +0 -0
  76. {aes_cli-0.4.0 → aes_cli-0.4.2}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aes-cli
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: CLI tool for the Agentic Engineering Standard
5
5
  Author: Hiro
6
6
  License: Apache-2.0
@@ -40,6 +40,9 @@ AES treats agent instructions, skills, permissions, and memory as **first-class
40
40
  ## Installation
41
41
 
42
42
  ```bash
43
+ pipx install aes-cli
44
+
45
+ # Or inside a virtual environment
43
46
  pip install aes-cli
44
47
  ```
45
48
 
@@ -7,6 +7,9 @@ AES treats agent instructions, skills, permissions, and memory as **first-class
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
+ pipx install aes-cli
11
+
12
+ # Or inside a virtual environment
10
13
  pip install aes-cli
11
14
  ```
12
15
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.4.0"
5
+ __version__ = "0.4.2"
@@ -30,6 +30,19 @@ console = Console()
30
30
  SYNC_MANIFEST = ".aes-sync.json"
31
31
 
32
32
 
33
+ def _validate_subpath(base: Path, child: Path) -> None:
34
+ """Ensure *child* resolves to a location under *base*.
35
+
36
+ Raises click.ClickException on path traversal attempts.
37
+ """
38
+ try:
39
+ child.resolve().relative_to(base.resolve())
40
+ except ValueError:
41
+ raise click.ClickException(
42
+ f"Path traversal blocked: {child} escapes {base}"
43
+ )
44
+
45
+
33
46
  def run_sync(
34
47
  project_root: Path,
35
48
  target_names: Optional[List[str]] = None,
@@ -60,6 +73,7 @@ def run_sync(
60
73
  for gf in sync_plan.files:
61
74
  if gf.action in ("create", "update"):
62
75
  full_path = project_root / gf.relative_path
76
+ _validate_subpath(project_root, full_path)
63
77
  full_path.parent.mkdir(parents=True, exist_ok=True)
64
78
  full_path.write_text(gf.content)
65
79
  sync_manifest["files"][gf.relative_path] = {
@@ -106,6 +120,7 @@ def _load_agent_context(project_root: Path) -> AgentContext:
106
120
  instructions: Optional[str] = None
107
121
  instructions_rel = agent_section.get("instructions", "instructions.md")
108
122
  instructions_path = agent_dir / instructions_rel
123
+ _validate_subpath(agent_dir, instructions_path)
109
124
  if instructions_path.exists():
110
125
  instructions = instructions_path.read_text()
111
126
 
@@ -114,6 +129,7 @@ def _load_agent_context(project_root: Path) -> AgentContext:
114
129
  orchestrator_rel = agent_section.get("orchestrator")
115
130
  if orchestrator_rel:
116
131
  orchestrator_path = agent_dir / orchestrator_rel
132
+ _validate_subpath(agent_dir, orchestrator_path)
117
133
  if orchestrator_path.exists():
118
134
  orchestrator = orchestrator_path.read_text()
119
135
 
@@ -125,12 +141,14 @@ def _load_agent_context(project_root: Path) -> AgentContext:
125
141
  runbook_rel = skill_ref.get("runbook")
126
142
  if runbook_rel:
127
143
  runbook_path = agent_dir / runbook_rel
144
+ _validate_subpath(agent_dir, runbook_path)
128
145
  if runbook_path.exists():
129
146
  skill_runbooks[skill_id] = runbook_path.read_text()
130
147
  # Load skill manifest for name/description/activation metadata
131
148
  manifest_rel = skill_ref.get("manifest")
132
149
  if manifest_rel:
133
150
  skill_manifest_path = agent_dir / manifest_rel
151
+ _validate_subpath(agent_dir, skill_manifest_path)
134
152
  if skill_manifest_path.exists():
135
153
  with open(skill_manifest_path) as f:
136
154
  skill_data = yaml.safe_load(f) or {}
@@ -154,6 +172,7 @@ def _load_agent_context(project_root: Path) -> AgentContext:
154
172
  permissions: Optional[dict] = None
155
173
  permissions_rel = agent_section.get("permissions", PERMISSIONS_FILE)
156
174
  permissions_path = agent_dir / permissions_rel
175
+ _validate_subpath(agent_dir, permissions_path)
157
176
  if permissions_path.exists():
158
177
  with open(permissions_path) as f:
159
178
  permissions = yaml.safe_load(f) or {}
@@ -163,6 +182,7 @@ def _load_agent_context(project_root: Path) -> AgentContext:
163
182
  for cmd_ref in manifest.get("commands", []):
164
183
  cmd_data = dict(cmd_ref)
165
184
  cmd_path = agent_dir / cmd_ref["path"]
185
+ _validate_subpath(agent_dir, cmd_path)
166
186
  if cmd_path.exists():
167
187
  cmd_data["content"] = cmd_path.read_text()
168
188
  else:
@@ -320,6 +340,7 @@ def sync_cmd(
320
340
 
321
341
  if gf.action in ("create", "update") and not dry_run:
322
342
  full_path = project_root / gf.relative_path
343
+ _validate_subpath(project_root, full_path)
323
344
  full_path.parent.mkdir(parents=True, exist_ok=True)
324
345
  full_path.write_text(gf.content)
325
346
  sync_manifest["files"][gf.relative_path] = {
@@ -190,7 +190,7 @@ MESSAGES: dict = {
190
190
  "init.specific_label": "{type}-specific",
191
191
  "init.synced_to": "Synced to {count} tool config(s):",
192
192
  "init.mcp_configured": "MCP: {file} configured (AES registry tools)",
193
- "init.mcp_install_hint": "Install MCP server: pip install aes-cli[mcp]",
193
+ "init.mcp_install_hint": "Install MCP server: pipx install aes-cli[mcp]",
194
194
  "init.next_steps": "Next: Start a new agent session, then type /setup to fine-tune{hint}.",
195
195
  "init.or_begin": ", or {trigger} to begin",
196
196
  "init.from_template": "Initialized from template:",
@@ -190,7 +190,7 @@ MESSAGES: dict = {
190
190
  "init.specific_label": "{type}向け",
191
191
  "init.synced_to": "{count} 件のツール設定に同期しました:",
192
192
  "init.mcp_configured": "MCP: {file} を設定しました(AESレジストリツール)",
193
- "init.mcp_install_hint": "MCPサーバーのインストール: pip install aes-cli[mcp]",
193
+ "init.mcp_install_hint": "MCPサーバーのインストール: pipx install aes-cli[mcp]",
194
194
  "init.next_steps": "次のステップ: 新しいエージェントセッションを開始し、/setup で調整{hint}。",
195
195
  "init.or_begin": "、または {trigger} で開始",
196
196
  "init.from_template": "テンプレートから初期化しました:",
@@ -203,11 +203,16 @@ def upload_package(
203
203
  tarball_data = tarball_path.read_bytes()
204
204
  sha = hashlib.sha256(tarball_data).hexdigest()
205
205
 
206
- # Upload tarball
206
+ # Upload tarball — server auto-updates index from X-AES-* headers
207
207
  upload_url = f"{base.rstrip('/')}/{PACKAGES_PATH}/{name}/{version}.tar.gz"
208
208
  req = urllib.request.Request(upload_url, data=tarball_data, method="PUT")
209
209
  req.add_header("Authorization", f"Bearer {token}")
210
210
  req.add_header("Content-Type", "application/gzip")
211
+ req.add_header("X-AES-Description", description)
212
+ req.add_header("X-AES-Type", pkg_type)
213
+ req.add_header("X-AES-Visibility", visibility)
214
+ if tags:
215
+ req.add_header("X-AES-Tags", ",".join(tags))
211
216
  try:
212
217
  urllib.request.urlopen(req, timeout=120)
213
218
  except urllib.error.HTTPError as exc:
@@ -216,51 +221,28 @@ def upload_package(
216
221
  f"Version {version} of '{name}' already exists in the registry. "
217
222
  "Bump the version in your skill manifest and try again."
218
223
  ) from None
219
- raise
224
+ # Wrap to avoid leaking auth headers in tracebacks
225
+ raise RuntimeError(
226
+ f"Registry upload failed (HTTP {exc.code}): {exc.reason}"
227
+ ) from None
220
228
 
221
- # Fetch current index
222
- try:
223
- index = fetch_index(registry_url)
224
- except (urllib.error.URLError, urllib.error.HTTPError):
225
- index = {"packages": {}}
229
+ from datetime import datetime, timezone
226
230
 
227
- # Update index
228
- packages = index.setdefault("packages", {})
229
- pkg = packages.setdefault(name, {
231
+ return {
230
232
  "description": description,
233
+ "type": pkg_type,
234
+ "visibility": visibility,
235
+ "tags": tags or [],
231
236
  "latest": version,
232
- "versions": {},
233
- })
234
- pkg["description"] = description
235
- pkg["type"] = pkg_type
236
- pkg["visibility"] = visibility
237
- if tags:
238
- pkg["tags"] = tags
239
-
240
- from datetime import datetime, timezone
241
- pkg["versions"][version] = {
242
- "url": f"{PACKAGES_PATH}/{name}/{version}.tar.gz",
243
- "sha256": sha,
244
- "published_at": datetime.now(timezone.utc).isoformat(),
237
+ "versions": {
238
+ version: {
239
+ "url": f"{PACKAGES_PATH}/{name}/{version}.tar.gz",
240
+ "sha256": sha,
241
+ "published_at": datetime.now(timezone.utc).isoformat(),
242
+ }
243
+ },
245
244
  }
246
245
 
247
- # Update "latest" if this version is higher
248
- try:
249
- if _parse_version(version) >= _parse_version(pkg.get("latest", "0.0.0")):
250
- pkg["latest"] = version
251
- except ValueError:
252
- pkg["latest"] = version
253
-
254
- # Upload updated index
255
- index_data = json.dumps(index, indent=2).encode()
256
- index_url = f"{base.rstrip('/')}/{INDEX_PATH}"
257
- req = urllib.request.Request(index_url, data=index_data, method="PUT")
258
- req.add_header("Authorization", f"Bearer {token}")
259
- req.add_header("Content-Type", "application/json")
260
- urllib.request.urlopen(req, timeout=30)
261
-
262
- return pkg
263
-
264
246
 
265
247
  def search_packages(
266
248
  query: str = "",
@@ -77,7 +77,8 @@ class ClaudeTarget(SyncTarget):
77
77
 
78
78
  # 3. .claude/commands/*.md (user-defined commands)
79
79
  for cmd in ctx.commands:
80
- rel_path = f".claude/commands/{cmd['id']}.md"
80
+ cmd_id = cmd['id'].replace("/", "-").replace("\\", "-")
81
+ rel_path = f".claude/commands/{cmd_id}.md"
81
82
  cmd_content = AES_SENTINEL_MD + "\n" + cmd.get("content", "")
82
83
  action = self._check_conflict(ctx.project_root, rel_path, force)
83
84
  plan.files.append(GeneratedFile(
@@ -89,7 +90,8 @@ class ClaudeTarget(SyncTarget):
89
90
 
90
91
  # 4. .claude/commands/skills/<id>.md (skill runbooks as slash commands)
91
92
  for skill_id, runbook in ctx.skill_runbooks.items():
92
- rel_path = f".claude/commands/skills/{skill_id}.md"
93
+ safe_id = skill_id.replace("/", "-").replace("\\", "-")
94
+ rel_path = f".claude/commands/skills/{safe_id}.md"
93
95
  skill_content = AES_SENTINEL_MD + "\n" + runbook
94
96
 
95
97
  # Append per-skill metadata (negative triggers, permissions)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aes-cli
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: CLI tool for the Agentic Engineering Standard
5
5
  Author: Hiro
6
6
  License: Apache-2.0
@@ -40,6 +40,9 @@ AES treats agent instructions, skills, permissions, and memory as **first-class
40
40
  ## Installation
41
41
 
42
42
  ```bash
43
+ pipx install aes-cli
44
+
45
+ # Or inside a virtual environment
43
46
  pip install aes-cli
44
47
  ```
45
48
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aes-cli"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "CLI tool for the Agentic Engineering Standard"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -331,7 +331,7 @@ class TestUploadPackage:
331
331
 
332
332
  assert result["visibility"] == "public"
333
333
 
334
- def test_500_still_raises_httperror(self, tmp_path):
334
+ def test_500_still_raises_error(self, tmp_path):
335
335
  tarball = tmp_path / "deploy-1.0.0.tar.gz"
336
336
  tarball.write_bytes(b"fake tarball content")
337
337
 
@@ -345,7 +345,7 @@ class TestUploadPackage:
345
345
 
346
346
  with patch.dict(os.environ, {"AES_REGISTRY_KEY": "test-token"}):
347
347
  with patch("aes.registry.urllib.request.urlopen", side_effect=err_500):
348
- with pytest.raises(urllib.error.HTTPError):
348
+ with pytest.raises(RuntimeError, match="HTTP 500"):
349
349
  upload_package(
350
350
  tarball_path=tarball,
351
351
  name="deploy",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes