aes-cli 0.5.0__tar.gz → 0.7.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.
Files changed (89) hide show
  1. {aes_cli-0.5.0 → aes_cli-0.7.0}/PKG-INFO +1 -1
  2. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/__init__.py +1 -1
  3. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/__main__.py +4 -0
  4. aes_cli-0.7.0/aes/commands/bom.py +131 -0
  5. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/init.py +8 -0
  6. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/inspect.py +42 -1
  7. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/publish.py +77 -9
  8. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/status.py +6 -1
  9. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/sync.py +30 -2
  10. aes_cli-0.7.0/aes/commands/upgrade.py +282 -0
  11. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/config.py +4 -0
  12. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/domains.py +201 -0
  13. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/i18n/_messages.py +49 -0
  14. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/i18n/ja.py +49 -0
  15. aes_cli-0.7.0/aes/migrations.py +72 -0
  16. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/agent.yaml.jinja +52 -2
  17. aes_cli-0.7.0/aes/scaffold/bom.yaml.jinja +27 -0
  18. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/instructions.md.jinja +4 -4
  19. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/permissions.yaml.jinja +30 -2
  20. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/skill.yaml.jinja +29 -2
  21. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/workflow.yaml.jinja +1 -1
  22. aes_cli-0.7.0/aes/schemas/agent.schema.json +355 -0
  23. aes_cli-0.7.0/aes/schemas/bom.schema.json +77 -0
  24. aes_cli-0.7.0/aes/schemas/decision-record.schema.json +78 -0
  25. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/schemas/permissions.schema.json +86 -0
  26. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/schemas/skill.schema.json +58 -0
  27. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/targets/__init__.py +2 -0
  28. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/targets/_composer.py +286 -0
  29. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/targets/claude.py +11 -7
  30. aes_cli-0.7.0/aes/targets/openclaw.py +507 -0
  31. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/validator.py +12 -1
  32. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes_cli.egg-info/PKG-INFO +1 -1
  33. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes_cli.egg-info/SOURCES.txt +10 -0
  34. {aes_cli-0.5.0 → aes_cli-0.7.0}/pyproject.toml +1 -1
  35. aes_cli-0.7.0/tests/test_bom.py +132 -0
  36. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_init.py +3 -3
  37. aes_cli-0.7.0/tests/test_openclaw_target.py +262 -0
  38. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_sync.py +34 -0
  39. aes_cli-0.7.0/tests/test_upgrade.py +430 -0
  40. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_validate.py +326 -0
  41. aes_cli-0.5.0/aes/schemas/agent.schema.json +0 -188
  42. {aes_cli-0.5.0 → aes_cli-0.7.0}/README.md +0 -0
  43. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/analyzer.py +0 -0
  44. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/__init__.py +0 -0
  45. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/install.py +0 -0
  46. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/search.py +0 -0
  47. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/commands/validate.py +0 -0
  48. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/frameworks.py +0 -0
  49. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/global_config.py +0 -0
  50. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/i18n/__init__.py +0 -0
  51. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/i18n/domains_ja.py +0 -0
  52. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/mcp_server.py +0 -0
  53. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/registry.py +0 -0
  54. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/agentignore.jinja +0 -0
  55. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/ja/instructions.md.jinja +0 -0
  56. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/ja/memory_command.md.jinja +0 -0
  57. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/ja/operations.md.jinja +0 -0
  58. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/ja/orchestrator.md.jinja +0 -0
  59. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/ja/setup.md.jinja +0 -0
  60. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/ja/skill.md.jinja +0 -0
  61. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/ja/workflow_command.md.jinja +0 -0
  62. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/local.example.yaml.jinja +0 -0
  63. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/local.yaml.jinja +0 -0
  64. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/memory_command.md.jinja +0 -0
  65. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/operations.md.jinja +0 -0
  66. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/orchestrator.md.jinja +0 -0
  67. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/setup.md.jinja +0 -0
  68. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/skill.md.jinja +0 -0
  69. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/scaffold/workflow_command.md.jinja +0 -0
  70. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/schemas/registry.schema.json +0 -0
  71. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/schemas/workflow.schema.json +0 -0
  72. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/targets/_base.py +0 -0
  73. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/targets/copilot.py +0 -0
  74. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/targets/cursor.py +0 -0
  75. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes/targets/windsurf.py +0 -0
  76. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes_cli.egg-info/dependency_links.txt +0 -0
  77. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes_cli.egg-info/entry_points.txt +0 -0
  78. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes_cli.egg-info/requires.txt +0 -0
  79. {aes_cli-0.5.0 → aes_cli-0.7.0}/aes_cli.egg-info/top_level.txt +0 -0
  80. {aes_cli-0.5.0 → aes_cli-0.7.0}/setup.cfg +0 -0
  81. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_analyzer.py +0 -0
  82. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_frameworks.py +0 -0
  83. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_inspect.py +0 -0
  84. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_install.py +0 -0
  85. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_mcp_server.py +0 -0
  86. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_publish.py +0 -0
  87. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_registry.py +0 -0
  88. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_search.py +0 -0
  89. {aes_cli-0.5.0 → aes_cli-0.7.0}/tests/test_status.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aes-cli
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: CLI tool for the Agentic Engineering Standard
5
5
  Author: Hiro
6
6
  License: Apache-2.0
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.5.0"
5
+ __version__ = "0.7.0"
@@ -16,6 +16,8 @@ from aes.commands.install import install_cmd
16
16
  from aes.commands.search import search_cmd
17
17
  from aes.commands.status import status_cmd
18
18
  from aes.commands.sync import sync_cmd
19
+ from aes.commands.upgrade import upgrade_cmd
20
+ from aes.commands.bom import bom_cmd
19
21
 
20
22
 
21
23
  def _prompt_language() -> None:
@@ -77,6 +79,8 @@ cli.add_command(install_cmd, "install")
77
79
  cli.add_command(sync_cmd, "sync")
78
80
  cli.add_command(status_cmd, "status")
79
81
  cli.add_command(search_cmd, "search")
82
+ cli.add_command(upgrade_cmd, "upgrade")
83
+ cli.add_command(bom_cmd, "bom")
80
84
 
81
85
 
82
86
  if __name__ == "__main__":
@@ -0,0 +1,131 @@
1
+ """aes bom — Display the Agent Bill of Materials."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import yaml
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ from aes.config import AGENT_DIR, BOM_FILE
13
+ from aes.i18n import t
14
+
15
+ console = Console()
16
+
17
+
18
+ def _load_bom(agent_dir: Path) -> dict:
19
+ """Load and return bom.yaml, or empty dict."""
20
+ bom_path = agent_dir / BOM_FILE
21
+ if not bom_path.exists():
22
+ return {}
23
+ with open(bom_path) as f:
24
+ data = yaml.safe_load(f)
25
+ return data if isinstance(data, dict) else {}
26
+
27
+
28
+ @click.command("bom")
29
+ @click.argument("path", default=".", type=click.Path(exists=True))
30
+ def bom_cmd(path: str) -> None:
31
+ """Display the Agent Bill of Materials (AI-BOM).
32
+
33
+ Shows all models, frameworks, tools, and data sources
34
+ declared in .agent/bom.yaml.
35
+
36
+ \b
37
+ Examples:
38
+ aes bom # current project
39
+ aes bom ./my-project # specific project
40
+ """
41
+ project_root = Path(path).resolve()
42
+ agent_dir = project_root / AGENT_DIR
43
+
44
+ if not agent_dir.exists():
45
+ console.print(f"[red]{t('common.error')}:[/] {t('common.no_agent_dir', agent_dir=AGENT_DIR, path=project_root)}")
46
+ raise SystemExit(1)
47
+
48
+ bom = _load_bom(agent_dir)
49
+ if not bom:
50
+ console.print(f"\n [dim]{t('bom.not_found')}[/]\n")
51
+ return
52
+
53
+ console.print(f"\n[bold]{t('bom.title')}[/] [dim](AES {bom.get('aes_bom', '?')})[/]\n")
54
+
55
+ # Models
56
+ models = bom.get("models", [])
57
+ if models:
58
+ table = Table(title=t("bom.models_table"), show_header=True, header_style="bold")
59
+ table.add_column(t("bom.col_name"), style="cyan")
60
+ table.add_column(t("bom.col_provider"))
61
+ table.add_column(t("bom.col_purpose"))
62
+ table.add_column(t("bom.col_license"))
63
+ for m in models:
64
+ table.add_row(
65
+ m.get("name", "?"),
66
+ m.get("provider", "?"),
67
+ m.get("purpose", "-"),
68
+ m.get("license", "-"),
69
+ )
70
+ console.print(table)
71
+ console.print()
72
+
73
+ # Frameworks
74
+ frameworks = bom.get("frameworks", [])
75
+ if frameworks:
76
+ table = Table(title=t("bom.frameworks_table"), show_header=True, header_style="bold")
77
+ table.add_column(t("bom.col_name"), style="cyan")
78
+ table.add_column(t("bom.col_version"))
79
+ table.add_column(t("bom.col_license"))
80
+ for fw in frameworks:
81
+ table.add_row(
82
+ fw.get("name", "?"),
83
+ fw.get("version", "-"),
84
+ fw.get("license", "-"),
85
+ )
86
+ console.print(table)
87
+ console.print()
88
+
89
+ # Tools
90
+ tools = bom.get("tools", [])
91
+ if tools:
92
+ table = Table(title=t("bom.tools_table"), show_header=True, header_style="bold")
93
+ table.add_column(t("bom.col_name"), style="cyan")
94
+ table.add_column(t("bom.col_type"))
95
+ table.add_column(t("bom.col_version"))
96
+ table.add_column(t("bom.col_source"))
97
+ for tool in tools:
98
+ table.add_row(
99
+ tool.get("name", "?"),
100
+ tool.get("type", "?"),
101
+ tool.get("version", "-"),
102
+ tool.get("source", "-"),
103
+ )
104
+ console.print(table)
105
+ console.print()
106
+
107
+ # Data Sources
108
+ data_sources = bom.get("data_sources", [])
109
+ if data_sources:
110
+ table = Table(title=t("bom.data_sources_table"), show_header=True, header_style="bold")
111
+ table.add_column(t("bom.col_name"), style="cyan")
112
+ table.add_column(t("bom.col_type"))
113
+ table.add_column(t("bom.col_uri"))
114
+ table.add_column(t("bom.col_license"))
115
+ for ds in data_sources:
116
+ table.add_row(
117
+ ds.get("name", "?"),
118
+ ds.get("type", "?"),
119
+ ds.get("uri", "-"),
120
+ ds.get("license", "-"),
121
+ )
122
+ console.print(table)
123
+ console.print()
124
+
125
+ # Summary
126
+ console.print(f"[bold]{t('bom.summary')}[/]")
127
+ console.print(f" {t('bom.models_count', count=len(models))}")
128
+ console.print(f" {t('bom.frameworks_count', count=len(frameworks))}")
129
+ console.print(f" {t('bom.tools_count', count=len(tools))}")
130
+ console.print(f" {t('bom.data_sources_count', count=len(data_sources))}")
131
+ console.print()
@@ -20,6 +20,8 @@ from rich.tree import Tree
20
20
  from aes.config import (
21
21
  AGENT_DIR,
22
22
  AGENTIGNORE_FILE,
23
+ BOM_FILE,
24
+ DECISIONS_DIR,
23
25
  LOCAL_EXAMPLE_FILE,
24
26
  LOCAL_FILE,
25
27
  SCAFFOLD_DIR,
@@ -252,6 +254,7 @@ def _get_agent_integrated_types() -> list:
252
254
  return [
253
255
  (t("init.type_ml"), "ml"),
254
256
  (t("init.type_research"), "research"),
257
+ (t("init.type_assistant"), "assistant"),
255
258
  (t("init.type_custom"), "other"),
256
259
  ]
257
260
 
@@ -620,6 +623,7 @@ def init_cmd(
620
623
  agent_dir.mkdir(exist_ok=True)
621
624
  (agent_dir / MEMORY_DIR).mkdir(exist_ok=True)
622
625
  (agent_dir / MEMORY_DIR / "sessions").mkdir(exist_ok=True)
626
+ (agent_dir / DECISIONS_DIR).mkdir(parents=True, exist_ok=True)
623
627
  (agent_dir / OVERRIDES_DIR).mkdir(exist_ok=True)
624
628
 
625
629
  if skills:
@@ -664,6 +668,10 @@ def init_cmd(
664
668
  content = _render_template(env, "permissions.yaml.jinja", context)
665
669
  (agent_dir / "permissions.yaml").write_text(content)
666
670
 
671
+ # bom.yaml
672
+ content = _render_template(env, "bom.yaml.jinja", context)
673
+ (agent_dir / BOM_FILE).write_text(content)
674
+
667
675
  # .agentignore
668
676
  agentignore_path = project_root / AGENTIGNORE_FILE
669
677
  if not agentignore_path.exists():
@@ -12,7 +12,7 @@ import yaml
12
12
  from rich.console import Console
13
13
  from rich.table import Table
14
14
 
15
- from aes.config import AGENT_DIR
15
+ from aes.config import AGENT_DIR, BOM_FILE, DECISIONS_DIR
16
16
  from aes.i18n import t
17
17
  from aes.registry import (
18
18
  fetch_index,
@@ -205,6 +205,47 @@ def _inspect_local(path: str) -> None:
205
205
  console.print(table)
206
206
  console.print()
207
207
 
208
+ # Models
209
+ models = manifest.get("models", [])
210
+ if models:
211
+ console.print(f"[bold]{t('inspect.models_section')}[/]")
212
+ for m in models:
213
+ purpose = m.get("purpose", "")
214
+ purpose_str = f" [dim]({purpose})[/]" if purpose else ""
215
+ console.print(f" {m.get('name', '?')} — {m.get('provider', '?')}{purpose_str}")
216
+ console.print()
217
+
218
+ # Provenance
219
+ provenance = manifest.get("provenance", {})
220
+ if provenance:
221
+ console.print(f"[bold]{t('inspect.provenance_section')}[/]")
222
+ if provenance.get("created_by"):
223
+ console.print(f" {t('inspect.provenance_created_by', value=provenance['created_by'])}")
224
+ if provenance.get("source"):
225
+ console.print(f" {t('inspect.provenance_source', value=provenance['source'])}")
226
+ console.print()
227
+
228
+ # BOM summary
229
+ bom_path = agent_dir / BOM_FILE
230
+ if bom_path.exists():
231
+ bom = _load_yaml(bom_path)
232
+ n_models = len(bom.get("models", []))
233
+ n_frameworks = len(bom.get("frameworks", []))
234
+ n_tools = len(bom.get("tools", []))
235
+ n_data = len(bom.get("data_sources", []))
236
+ console.print(f"[bold]{t('inspect.bom_section')}[/]")
237
+ console.print(f" {t('inspect.bom_summary', models=n_models, frameworks=n_frameworks, tools=n_tools, data=n_data)}")
238
+ console.print()
239
+
240
+ # Decision records count
241
+ decisions_dir = agent_dir / DECISIONS_DIR
242
+ if decisions_dir.exists() and decisions_dir.is_dir():
243
+ dr_count = len(list(decisions_dir.glob("*.yaml")))
244
+ if dr_count > 0:
245
+ console.print(f"[bold]{t('inspect.decisions_section')}[/]")
246
+ console.print(f" {t('inspect.decisions_count', count=dr_count)}")
247
+ console.print()
248
+
208
249
  # Summary
209
250
  console.print(f"[bold]{t('inspect.summary')}[/]")
210
251
  console.print(f" {t('inspect.skills_count', count=len(skills))}")
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import fnmatch
6
+ import hashlib
7
+ import json
6
8
  import shutil
7
9
  import sys
8
10
  import tarfile
@@ -14,7 +16,7 @@ import click
14
16
  import yaml
15
17
  from rich.console import Console
16
18
 
17
- from aes.config import AGENT_DIR, MANIFEST_FILE
19
+ from aes.config import AGENT_DIR, BOM_FILE, MANIFEST_FILE
18
20
  from aes.i18n import t
19
21
 
20
22
  console = Console()
@@ -171,6 +173,53 @@ def _validate_before_publish(project_root: Path) -> bool:
171
173
  return True
172
174
 
173
175
 
176
+ def _media_type_for_path(path: str) -> str:
177
+ """Return a media type string for a file path."""
178
+ if path.endswith(".yaml") or path.endswith(".yml"):
179
+ return "application/vnd.aes.agent-config.v1+yaml"
180
+ if path.endswith(".md"):
181
+ return "text/markdown"
182
+ if path.endswith(".json"):
183
+ return "application/json"
184
+ return "application/octet-stream"
185
+
186
+
187
+ def _build_package_manifest(
188
+ name: str,
189
+ version: str,
190
+ aes_version: str,
191
+ pkg_type: str,
192
+ files: List[tuple],
193
+ ) -> dict:
194
+ """Build an aes-manifest.json dict.
195
+
196
+ *files* is a list of ``(arcname, file_path)`` tuples.
197
+ """
198
+ layers = []
199
+ for arcname, file_path in files:
200
+ data = file_path.read_bytes()
201
+ digest = hashlib.sha256(data).hexdigest()
202
+ layers.append({
203
+ "mediaType": _media_type_for_path(arcname),
204
+ "digest": f"sha256:{digest}",
205
+ "size": len(data),
206
+ "path": arcname,
207
+ })
208
+
209
+ return {
210
+ "schemaVersion": 1,
211
+ "mediaType": "application/vnd.aes.package.v1+tar+gzip",
212
+ "config": {
213
+ "name": name,
214
+ "version": version,
215
+ "type": pkg_type,
216
+ "aes": aes_version,
217
+ },
218
+ "layers": layers,
219
+ "signature": None,
220
+ }
221
+
222
+
174
223
  def _publish_template_dir(
175
224
  project_root: Path,
176
225
  output_dir: Path,
@@ -194,6 +243,7 @@ def _publish_template_dir(
194
243
 
195
244
  name = manifest.get("name", "unknown")
196
245
  version = manifest.get("version", "0.0.0")
246
+ aes_version = manifest.get("aes", "1.0")
197
247
 
198
248
  # Build exclusion list
199
249
  if include_all:
@@ -208,17 +258,35 @@ def _publish_template_dir(
208
258
  tarball_name = f"{name}-{version}.tar.gz"
209
259
  tarball_path = output_dir / tarball_name
210
260
 
261
+ # Collect files and build package manifest
262
+ included_files: List[tuple] = [] # (rel_path_in_agent, absolute_path)
263
+
264
+ for file_path in sorted(agent_dir.rglob("*")):
265
+ if not file_path.is_file():
266
+ continue
267
+ rel = file_path.relative_to(agent_dir)
268
+ rel_str = str(rel)
269
+ if _is_excluded(rel_str, excludes):
270
+ continue
271
+ included_files.append((f"{AGENT_DIR}/{rel_str}", file_path))
272
+
273
+ # Generate aes-manifest.json
274
+ pkg_manifest = _build_package_manifest(
275
+ name, version, aes_version, "template", included_files,
276
+ )
277
+
211
278
  with tarfile.open(tarball_path, "w:gz") as tar:
212
- for file_path in sorted(agent_dir.rglob("*")):
213
- if not file_path.is_file():
214
- continue
215
- rel = file_path.relative_to(agent_dir)
216
- rel_str = str(rel)
217
- if _is_excluded(rel_str, excludes):
218
- continue
219
- arcname = f"{name}/{AGENT_DIR}/{rel_str}"
279
+ for rel_path, file_path in included_files:
280
+ arcname = f"{name}/{rel_path}"
220
281
  tar.add(file_path, arcname=arcname)
221
282
 
283
+ # Add aes-manifest.json at the package root
284
+ manifest_json = json.dumps(pkg_manifest, indent=2).encode()
285
+ import io
286
+ info = tarfile.TarInfo(name=f"{name}/aes-manifest.json")
287
+ info.size = len(manifest_json)
288
+ tar.addfile(info, io.BytesIO(manifest_json))
289
+
222
290
  return tarball_path
223
291
 
224
292
 
@@ -70,7 +70,12 @@ def status_cmd(path: str) -> None:
70
70
 
71
71
  for name in TARGET_NAMES:
72
72
  adapter = TARGETS[name]()
73
- plan = adapter.plan(ctx, force=True)
73
+ try:
74
+ plan = adapter.plan(ctx, force=True)
75
+ except Exception:
76
+ # Target-specific validation failure (e.g. openclaw requires
77
+ # identity/model) — skip incompatible targets silently in status.
78
+ continue
74
79
  for gf in plan.files:
75
80
  would_generate[gf.relative_path] = gf.content
76
81
 
@@ -64,7 +64,15 @@ def run_sync(
64
64
  all_plans: List[SyncPlan] = []
65
65
  for name in selected:
66
66
  adapter = TARGETS[name]()
67
- all_plans.append(adapter.plan(ctx, force))
67
+ try:
68
+ all_plans.append(adapter.plan(ctx, force))
69
+ except click.ClickException:
70
+ # Target-specific validation failure — skip when syncing
71
+ # multiple targets (e.g. openclaw requires identity/model
72
+ # which non-assistant projects won't have).
73
+ if len(selected) == 1:
74
+ raise
75
+ continue
68
76
 
69
77
  sync_manifest = _load_sync_manifest(project_root)
70
78
  written = 0
@@ -158,6 +166,14 @@ def _load_agent_context(project_root: Path) -> AgentContext:
158
166
  "negative_triggers": skill_data.get("negative_triggers", []),
159
167
  "activation": skill_data.get("activation", "explicit"),
160
168
  "allowed_tools": skill_data.get("allowed_tools"),
169
+ "version": skill_data.get("version", "0.1.0"),
170
+ "emoji": skill_data.get("emoji", ""),
171
+ "license": skill_data.get("license", "MIT"),
172
+ "user_invocable": skill_data.get("user_invocable", True),
173
+ "primary_env": skill_data.get("primary_env", ""),
174
+ "requires_bins": (skill_data.get("requires") or {}).get("bins", []),
175
+ "requires_env": (skill_data.get("requires") or {}).get("env", []),
176
+ "mcp_server": skill_data.get("mcp_server"),
161
177
  }
162
178
  if skill_id not in skill_metadata:
163
179
  skill_metadata[skill_id] = {
@@ -305,7 +321,19 @@ def sync_cmd(
305
321
  all_plans: List[SyncPlan] = []
306
322
  for target_name in selected:
307
323
  adapter = TARGETS[target_name]()
308
- sync_plan = adapter.plan(ctx, force)
324
+ try:
325
+ sync_plan = adapter.plan(ctx, force)
326
+ except click.ClickException as exc:
327
+ # Target-specific validation failure (e.g. openclaw requires
328
+ # identity/model). When syncing a single explicit target, re-raise
329
+ # so the user sees the error. When syncing all targets, skip and
330
+ # warn — not every project is compatible with every target.
331
+ if len(selected) == 1:
332
+ raise
333
+ console.print(
334
+ f" [yellow]⚠ {target_name}:[/] {exc.format_message()}"
335
+ )
336
+ continue
309
337
  all_plans.append(sync_plan)
310
338
 
311
339
  # Execute plans