10xgraph-api 0.7.0__py3-none-any.whl

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 (203) hide show
  1. 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
  2. 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
  3. 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
  4. 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
  5. 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
  6. 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
  7. agentflow_cli/__init__.py +69 -0
  8. tenxgraph_api/__init__.py +23 -0
  9. tenxgraph_api/cli/__init__.py +6 -0
  10. tenxgraph_api/cli/capabilities.py +127 -0
  11. tenxgraph_api/cli/commands/__init__.py +49 -0
  12. tenxgraph_api/cli/commands/api.py +262 -0
  13. tenxgraph_api/cli/commands/audit.py +213 -0
  14. tenxgraph_api/cli/commands/build.py +305 -0
  15. tenxgraph_api/cli/commands/config.py +71 -0
  16. tenxgraph_api/cli/commands/demo.py +93 -0
  17. tenxgraph_api/cli/commands/eval.py +909 -0
  18. tenxgraph_api/cli/commands/init.py +531 -0
  19. tenxgraph_api/cli/commands/skills.py +532 -0
  20. tenxgraph_api/cli/commands/test.py +113 -0
  21. tenxgraph_api/cli/commands/version.py +47 -0
  22. tenxgraph_api/cli/config_editor/__init__.py +15 -0
  23. tenxgraph_api/cli/config_editor/schema.py +566 -0
  24. tenxgraph_api/cli/config_editor/server.py +240 -0
  25. tenxgraph_api/cli/config_editor/static/app.css +2 -0
  26. tenxgraph_api/cli/config_editor/static/app.js +3 -0
  27. tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
  28. tenxgraph_api/cli/config_editor/static/index.html +20 -0
  29. tenxgraph_api/cli/config_editor/store.py +120 -0
  30. tenxgraph_api/cli/config_editor/validation.py +377 -0
  31. tenxgraph_api/cli/constants.py +112 -0
  32. tenxgraph_api/cli/context.py +46 -0
  33. tenxgraph_api/cli/core/__init__.py +0 -0
  34. tenxgraph_api/cli/core/animations.py +550 -0
  35. tenxgraph_api/cli/core/config.py +279 -0
  36. tenxgraph_api/cli/core/output.py +625 -0
  37. tenxgraph_api/cli/core/prompts.py +220 -0
  38. tenxgraph_api/cli/core/screen.py +287 -0
  39. tenxgraph_api/cli/core/steps.py +621 -0
  40. tenxgraph_api/cli/core/theme.py +242 -0
  41. tenxgraph_api/cli/core/validation.py +251 -0
  42. tenxgraph_api/cli/exceptions.py +129 -0
  43. tenxgraph_api/cli/logger.py +84 -0
  44. tenxgraph_api/cli/main.py +936 -0
  45. tenxgraph_api/cli/templates/__init__.py +0 -0
  46. tenxgraph_api/cli/templates/defaults.py +844 -0
  47. tenxgraph_api/cli/templates/dev/.env.example +1 -0
  48. tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
  49. tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
  50. tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
  51. tenxgraph_api/cli/templates/prod/.env.example +75 -0
  52. tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
  53. tenxgraph_api/cli/templates/prod/.python-version +1 -0
  54. tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
  55. tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
  56. tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
  57. tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
  58. tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
  59. tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
  60. tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
  61. tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
  62. tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
  63. tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
  64. tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
  65. tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
  66. tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
  67. tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
  68. tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
  69. tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
  70. tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
  71. tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
  72. tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
  73. tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
  74. tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
  75. tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
  76. tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
  77. tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
  78. tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
  79. tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
  80. tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
  81. tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
  82. tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
  83. tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
  84. tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
  85. tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
  86. tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
  87. tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
  88. tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
  89. tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
  90. tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
  91. tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
  92. tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
  93. tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
  94. tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
  95. tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
  96. tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
  97. tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
  98. tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
  99. tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
  100. tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
  101. tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
  102. tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
  103. tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
  104. tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
  105. tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
  106. tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
  107. tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
  108. tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
  109. tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
  110. tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
  111. tenxgraph_api/py.typed +0 -0
  112. tenxgraph_api/src/__init__.py +0 -0
  113. tenxgraph_api/src/app/__init__.py +13 -0
  114. tenxgraph_api/src/app/core/__init__.py +15 -0
  115. tenxgraph_api/src/app/core/auth/__init__.py +0 -0
  116. tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
  117. tenxgraph_api/src/app/core/auth/authorization.py +379 -0
  118. tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
  119. tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
  120. tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
  121. tenxgraph_api/src/app/core/auth/permissions.py +354 -0
  122. tenxgraph_api/src/app/core/auth/request_config.py +130 -0
  123. tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
  124. tenxgraph_api/src/app/core/config/__init__.py +0 -0
  125. tenxgraph_api/src/app/core/config/graph_config.py +532 -0
  126. tenxgraph_api/src/app/core/config/media_settings.py +72 -0
  127. tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
  128. tenxgraph_api/src/app/core/config/settings.py +202 -0
  129. tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
  130. tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
  131. tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
  132. tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
  133. tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
  134. tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
  135. tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
  136. tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
  137. tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
  138. tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
  139. tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
  140. tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
  141. tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
  142. tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
  143. tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
  144. tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
  145. tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
  146. tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
  147. tenxgraph_api/src/app/core/utils/__init__.py +14 -0
  148. tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
  149. tenxgraph_api/src/app/loader.py +516 -0
  150. tenxgraph_api/src/app/main.py +180 -0
  151. tenxgraph_api/src/app/routers/__init__.py +4 -0
  152. tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
  153. tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
  154. tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
  155. tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
  156. tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
  157. tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
  158. tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
  159. tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
  160. tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
  161. tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
  162. tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
  163. tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
  164. tenxgraph_api/src/app/routers/evals/router.py +38 -0
  165. tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
  166. tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
  167. tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
  168. tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
  169. tenxgraph_api/src/app/routers/graph/router.py +821 -0
  170. tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
  171. tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
  172. tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
  173. tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
  174. tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
  175. tenxgraph_api/src/app/routers/media/__init__.py +506 -0
  176. tenxgraph_api/src/app/routers/media/router.py +242 -0
  177. tenxgraph_api/src/app/routers/media/schemas.py +39 -0
  178. tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
  179. tenxgraph_api/src/app/routers/ping/router.py +28 -0
  180. tenxgraph_api/src/app/routers/setup_router.py +60 -0
  181. tenxgraph_api/src/app/routers/store/__init__.py +4 -0
  182. tenxgraph_api/src/app/routers/store/router.py +200 -0
  183. tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
  184. tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
  185. tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
  186. tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
  187. tenxgraph_api/src/app/tasks/__init__.py +0 -0
  188. tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
  189. tenxgraph_api/src/app/utils/__init__.py +12 -0
  190. tenxgraph_api/src/app/utils/callable_helper.py +26 -0
  191. tenxgraph_api/src/app/utils/media/__init__.py +15 -0
  192. tenxgraph_api/src/app/utils/media/extractor.py +172 -0
  193. tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
  194. tenxgraph_api/src/app/utils/parse_output.py +17 -0
  195. tenxgraph_api/src/app/utils/response_helper.py +129 -0
  196. tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
  197. tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
  198. tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
  199. tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
  200. tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
  201. tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
  202. tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
  203. tenxgraph_api/src/app/worker.py +91 -0
@@ -0,0 +1,532 @@
1
+ """Skills command implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+ from typing import Any, Literal
11
+
12
+ from tenxgraph_api.cli.commands import BaseCommand
13
+ from tenxgraph_api.cli.constants import CLI_VERSION
14
+ from tenxgraph_api.cli.core.prompts import Choice
15
+ from tenxgraph_api.cli.exceptions import FileOperationError, ValidationError
16
+
17
+
18
+ _MANIFEST_FILENAME = ".10xgraph-skill.json"
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class _InstallArtifact:
23
+ """Describes one file or folder installed for an agent."""
24
+
25
+ kind: Literal["folder", "file"]
26
+ install_relpath: str
27
+ source_relpath: str
28
+ manifest: bool = False
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class _AgentTarget:
33
+ """Describes how the bundled skill is materialised for one agent."""
34
+
35
+ name: str
36
+ artifacts: tuple[_InstallArtifact, ...]
37
+
38
+ @property
39
+ def kind(self) -> str:
40
+ kinds = {artifact.kind for artifact in self.artifacts}
41
+ if len(kinds) == 1:
42
+ return next(iter(kinds))
43
+ return "file+folder"
44
+
45
+
46
+ # Every agent gets the same spec-conformant skill folder (SKILL.md + references/);
47
+ # the SKILL.md uses paths relative to the skill directory, so one copy works in
48
+ # any install location.
49
+ _SKILL_SOURCE = "10xgraph"
50
+
51
+ _TARGETS: tuple[_AgentTarget, ...] = (
52
+ _AgentTarget(
53
+ name="Codex",
54
+ artifacts=(
55
+ _InstallArtifact(
56
+ kind="folder",
57
+ install_relpath=".agents/skills/10xgraph",
58
+ source_relpath=_SKILL_SOURCE,
59
+ manifest=True,
60
+ ),
61
+ ),
62
+ ),
63
+ _AgentTarget(
64
+ name="Claude",
65
+ artifacts=(
66
+ _InstallArtifact(
67
+ kind="folder",
68
+ install_relpath=".claude/skills/10xgraph",
69
+ source_relpath=_SKILL_SOURCE,
70
+ manifest=True,
71
+ ),
72
+ ),
73
+ ),
74
+ _AgentTarget(
75
+ name="GitHub",
76
+ artifacts=(
77
+ _InstallArtifact(
78
+ kind="file",
79
+ install_relpath=".github/instructions/10xgraph.instructions.md",
80
+ source_relpath="copilot/10xgraph.instructions.md",
81
+ ),
82
+ _InstallArtifact(
83
+ kind="folder",
84
+ install_relpath=".github/skills/10xgraph",
85
+ source_relpath=_SKILL_SOURCE,
86
+ manifest=True,
87
+ ),
88
+ ),
89
+ ),
90
+ )
91
+
92
+ _AGENT_LOOKUP: dict[str, _AgentTarget] = {
93
+ **{t.name.lower(): t for t in _TARGETS},
94
+ "1": _TARGETS[0],
95
+ "2": _TARGETS[1],
96
+ "3": _TARGETS[2],
97
+ }
98
+
99
+
100
+ class SkillsCommand(BaseCommand):
101
+ """Install bundled 10xGraph skills, or validate skills against the spec."""
102
+
103
+ def execute(
104
+ self,
105
+ agent: str | None = None,
106
+ path: str = ".",
107
+ force: bool = False,
108
+ all_agents: bool = False,
109
+ list_agents: bool = False,
110
+ validate_paths: list[str] | None = None,
111
+ **kwargs: Any,
112
+ ) -> int:
113
+ """Execute the skills command.
114
+
115
+ Args:
116
+ agent: Target agent name or menu number.
117
+ path: Project directory where the agent skill should be installed.
118
+ force: Overwrite an existing installation.
119
+ all_agents: Install for every supported agent.
120
+ list_agents: Print supported agents and exit.
121
+ validate_paths: Skill directories (or folders of skills) to validate
122
+ against the Agent Skills specification instead of installing.
123
+ **kwargs: Additional arguments.
124
+
125
+ Returns:
126
+ Exit code.
127
+ """
128
+ if validate_paths:
129
+ return self._run_validate(validate_paths)
130
+ return self._run_install(
131
+ agent, path, force=force, all_agents=all_agents, list_agents=list_agents
132
+ )
133
+
134
+ def _run_validate(self, validate_paths: list[str]) -> int:
135
+ try:
136
+ self.output.command_header(
137
+ "skills",
138
+ "Validate skills against the Agent Skills specification (agentskills.io).",
139
+ color="magenta",
140
+ )
141
+ return self._validate(validate_paths)
142
+ except ValidationError as e:
143
+ return self.handle_error(e)
144
+
145
+ def _run_install(
146
+ self,
147
+ agent: str | None,
148
+ path: str,
149
+ *,
150
+ force: bool,
151
+ all_agents: bool,
152
+ list_agents: bool,
153
+ ) -> int:
154
+ try:
155
+ self.output.command_header(
156
+ "skills",
157
+ "Install bundled 10xGraph skills for Codex, Claude, or GitHub Copilot.",
158
+ color="magenta",
159
+ )
160
+
161
+ if list_agents:
162
+ self._print_agents()
163
+ return 0
164
+
165
+ if all_agents and agent:
166
+ raise ValidationError("--all cannot be combined with --agent.", field="agent")
167
+
168
+ templates_root = self._templates_root()
169
+ project_root = self._safe_project_root(path)
170
+
171
+ if all_agents:
172
+ targets: tuple[_AgentTarget, ...] = _TARGETS
173
+ elif agent:
174
+ targets = (self._normalize_agent(agent),)
175
+ else:
176
+ selected = self._choose_agents(project_root)
177
+ if selected is None:
178
+ self.output.info("Cancelled.", emoji=False)
179
+ return 0
180
+ targets = selected
181
+
182
+ force = force or self._confirm_overwrite(project_root, targets, force=force)
183
+ return self._install_targets(
184
+ templates_root,
185
+ project_root,
186
+ targets,
187
+ force=force,
188
+ # Naming one agent is a request for that agent specifically, so a
189
+ # collision is an error. A set the user did not enumerate — --all,
190
+ # or a multi-select — skips what is already there instead.
191
+ strict=bool(agent),
192
+ )
193
+
194
+ except (FileOperationError, ValidationError) as e:
195
+ return self.handle_error(e)
196
+ except OSError as e:
197
+ file_error = FileOperationError(f"Failed to install 10xGraph skills: {e}")
198
+ file_error.__cause__ = e
199
+ return self.handle_error(file_error)
200
+
201
+ # ------------------------------------------------------------------
202
+ # Validation
203
+ # ------------------------------------------------------------------
204
+
205
+ def _validate(self, paths: list[str]) -> int:
206
+ """Validate each skill found under *paths*. Returns 1 if any is invalid."""
207
+ try:
208
+ # Not in any released core yet (<= 0.9.2); the ImportError branch covers that.
209
+ from tenxgraph.core.skills import validate_skill # type: ignore[attr-defined]
210
+ from tenxgraph.core.skills.loader import ( # type: ignore[attr-defined]
211
+ iter_skill_dirs,
212
+ )
213
+ except ImportError as exc:
214
+ raise ValidationError(
215
+ "Skill validation needs a 10xgraph release that ships "
216
+ "tenxgraph.core.skills.validate_skill. Upgrade 10xgraph.",
217
+ field="validate",
218
+ ) from exc
219
+
220
+ rows: list[list[str]] = []
221
+ invalid = 0
222
+ checked = 0
223
+ for raw_path in paths:
224
+ skill_dirs = iter_skill_dirs(raw_path)
225
+ if not skill_dirs:
226
+ self.output.error(
227
+ f"No skill found at {raw_path}: expected a SKILL.md in it or in its "
228
+ "subdirectories."
229
+ )
230
+ invalid += 1
231
+ continue
232
+
233
+ for skill_dir in skill_dirs:
234
+ checked += 1
235
+ issues = validate_skill(skill_dir)
236
+ errors = [issue for issue in issues if issue.level == "error"]
237
+ warnings = [issue for issue in issues if issue.level == "warning"]
238
+ for issue in errors:
239
+ self.output.error(f"{skill_dir.name}: {issue.message}")
240
+ for issue in warnings:
241
+ self.output.warning(f"{skill_dir.name}: {issue.message}")
242
+ if errors:
243
+ invalid += 1
244
+ status = "invalid" if errors else "valid with warnings" if warnings else "valid"
245
+ rows.append([str(skill_dir), status, str(len(errors)), str(len(warnings))])
246
+
247
+ if rows:
248
+ self.output.print_table(
249
+ ["Skill", "Status", "Errors", "Warnings"], rows, title="Agent Skills validation"
250
+ )
251
+ if invalid:
252
+ self.output.error(f"{invalid} skill(s) failed validation.")
253
+ return 1
254
+ self.output.success(f"{checked} skill(s) conform to the Agent Skills specification.")
255
+ return 0
256
+
257
+ # ------------------------------------------------------------------
258
+ # Selection
259
+ # ------------------------------------------------------------------
260
+
261
+ def _choose_agents(self, project_root: Path) -> tuple[_AgentTarget, ...] | None:
262
+ """Offer a space-to-toggle list of agents. Returns None if cancelled."""
263
+ prompts = self.output.prompts()
264
+ prompts.require_interactive(
265
+ field="agent",
266
+ alternatives="Pass --agent codex|claude|github, or --all.",
267
+ )
268
+
269
+ choices = [
270
+ Choice(
271
+ value=target.name,
272
+ title=target.name,
273
+ description=self._choice_description(project_root, target),
274
+ # Pre-check what is already installed, so confirming without
275
+ # touching anything reinstalls exactly what is already there.
276
+ checked=bool(self._existing_paths(project_root, target)),
277
+ )
278
+ for target in _TARGETS
279
+ ]
280
+ names = prompts.checkbox(
281
+ "Which agents should get the 10xGraph skill?",
282
+ choices,
283
+ validate=lambda selected: bool(selected) or "Select at least one agent.",
284
+ )
285
+ if names is None:
286
+ return None
287
+ return tuple(_AGENT_LOOKUP[name.lower()] for name in names)
288
+
289
+ def _choice_description(self, project_root: Path, target: _AgentTarget) -> str:
290
+ install_root = target.artifacts[0].install_relpath
291
+ if self._existing_paths(project_root, target):
292
+ return f"{install_root} (installed)"
293
+ return install_root
294
+
295
+ @staticmethod
296
+ def _existing_paths(project_root: Path, target: _AgentTarget) -> list[Path]:
297
+ return [
298
+ project_root / artifact.install_relpath
299
+ for artifact in target.artifacts
300
+ if (project_root / artifact.install_relpath).exists()
301
+ ]
302
+
303
+ def _confirm_overwrite(
304
+ self,
305
+ project_root: Path,
306
+ targets: tuple[_AgentTarget, ...],
307
+ *,
308
+ force: bool,
309
+ ) -> bool:
310
+ """Offer to overwrite in place of failing, when a human is present."""
311
+ if force:
312
+ return False
313
+ occupied = [t.name for t in targets if self._existing_paths(project_root, t)]
314
+ if not occupied:
315
+ return False
316
+
317
+ prompts = self.output.prompts()
318
+ if not prompts.interactive:
319
+ # Non-interactive callers keep the previous contract: refuse and
320
+ # point at --force rather than silently replacing files.
321
+ return False
322
+ return bool(
323
+ prompts.confirm(
324
+ f"Overwrite the existing install for {', '.join(occupied)}?",
325
+ default=True,
326
+ )
327
+ )
328
+
329
+ # ------------------------------------------------------------------
330
+ # Installation
331
+ # ------------------------------------------------------------------
332
+
333
+ def _install_targets(
334
+ self,
335
+ templates_root: Path,
336
+ project_root: Path,
337
+ targets: tuple[_AgentTarget, ...],
338
+ *,
339
+ force: bool,
340
+ strict: bool,
341
+ ) -> int:
342
+ installed: list[str] = []
343
+ skipped: list[str] = []
344
+ failed: list[str] = []
345
+
346
+ timeline = self.output.timeline(
347
+ "Installing 10xGraph skills",
348
+ steps=tuple((target.name.lower(), f"{target.name} skill") for target in targets),
349
+ )
350
+ with timeline:
351
+ for target in targets:
352
+ with timeline.step(target.name.lower()) as step:
353
+ existing = self._existing_paths(project_root, target)
354
+ if existing and not force:
355
+ if strict:
356
+ paths = ", ".join(str(dest) for dest in existing)
357
+ raise FileOperationError(
358
+ f"Skill already installed at {paths}. Use --force to overwrite.",
359
+ file_path=str(existing[0]),
360
+ )
361
+ step.skip("already installed — pass --force to overwrite")
362
+ skipped.append(target.name)
363
+ continue
364
+ try:
365
+ written = self._install_one(
366
+ templates_root, project_root, target, force=force
367
+ )
368
+ except (FileOperationError, OSError, UnicodeError) as exc:
369
+ # Record and continue: one unwritable target should not
370
+ # cancel the agents the user also asked for.
371
+ self.logger.error("Install failed for %s: %s", target.name, exc)
372
+ failed.append(f"{target.name}: {exc}")
373
+ step.fail(str(exc))
374
+ continue
375
+ installed.append(target.name)
376
+ step.detail(written[0])
377
+
378
+ for target in targets:
379
+ if target.name in installed:
380
+ self._print_activation_hint(target)
381
+
382
+ if skipped:
383
+ self.output.warning(
384
+ "Skipped existing installs (use --force to overwrite): " + ", ".join(skipped)
385
+ )
386
+ if failed:
387
+ self.output.error("Failed installs: " + "; ".join(failed))
388
+ if not installed:
389
+ return 1
390
+
391
+ self.output.completion_screen(
392
+ "Skills installed" if installed else "Nothing to install",
393
+ f"{len(installed)} agent(s) ready" if installed else "Every selection was skipped",
394
+ details={
395
+ "Installed": ", ".join(installed) or "none",
396
+ "Skipped": ", ".join(skipped) or "none",
397
+ "Project": project_root,
398
+ },
399
+ next_steps=["Restart your coding agent so it loads the new skill directory."]
400
+ if installed
401
+ else [],
402
+ )
403
+ return 0
404
+
405
+ def _install_one(
406
+ self,
407
+ templates_root: Path,
408
+ project_root: Path,
409
+ target: _AgentTarget,
410
+ *,
411
+ force: bool,
412
+ ) -> list[str]:
413
+ installs = [
414
+ (
415
+ artifact,
416
+ templates_root / artifact.source_relpath,
417
+ project_root / artifact.install_relpath,
418
+ )
419
+ for artifact in target.artifacts
420
+ ]
421
+ for _artifact, source, _dest in installs:
422
+ if not source.exists():
423
+ raise FileOperationError(
424
+ f"Bundled skills template not found: {source}", file_path=str(source)
425
+ )
426
+
427
+ existing = [dest for _artifact, _source, dest in installs if dest.exists()]
428
+ if existing and not force:
429
+ paths = ", ".join(str(dest) for dest in existing)
430
+ raise FileOperationError(
431
+ f"Skill already installed at {paths}. Use --force to overwrite.",
432
+ file_path=str(existing[0]),
433
+ )
434
+
435
+ for _artifact, _source, dest in installs:
436
+ if dest.exists():
437
+ if dest.is_dir():
438
+ shutil.rmtree(dest)
439
+ else:
440
+ dest.unlink()
441
+
442
+ installed_paths: list[str] = []
443
+ for artifact, source, dest in installs:
444
+ dest.parent.mkdir(parents=True, exist_ok=True)
445
+
446
+ if artifact.kind == "folder":
447
+ shutil.copytree(
448
+ source,
449
+ dest,
450
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store"),
451
+ )
452
+ if artifact.manifest:
453
+ self._write_manifest(dest, target.name)
454
+ else:
455
+ shutil.copyfile(source, dest)
456
+ installed_paths.append(str(dest))
457
+
458
+ return installed_paths
459
+
460
+ def _print_activation_hint(self, target: _AgentTarget) -> None:
461
+ """Tell the user how to make the freshly installed skill take effect.
462
+
463
+ Coding agents load skills at session start. When ``10xgraph skills``
464
+ creates the skills directory for the first time during a running session,
465
+ the agent does not watch the new top-level directory until it is
466
+ restarted, so the skill appears "installed but unused". This note makes
467
+ the required restart explicit.
468
+ """
469
+ if target.name == "Claude":
470
+ note = (
471
+ "Restart Claude Code (or run /exit then `claude`) so it loads the new "
472
+ ".claude/skills/ directory. Claude auto-invokes the skill from its "
473
+ "description; type /10xgraph to run it manually."
474
+ )
475
+ elif target.name == "Codex":
476
+ note = (
477
+ "Restart Codex so it picks up the new .agents/skills/ directory. Codex "
478
+ "auto-selects the skill when your task matches its description."
479
+ )
480
+ else: # GitHub Copilot
481
+ note = (
482
+ "Restart GitHub Copilot / your editor so it loads the new "
483
+ ".github/skills/ directory and .github/instructions/ file."
484
+ )
485
+ self.output.warning(f"Activate: {note}")
486
+
487
+ def _write_manifest(self, target_dir: Path, agent_name: str) -> None:
488
+ manifest = {
489
+ "agent": agent_name,
490
+ "cli_version": CLI_VERSION,
491
+ "installed_at": datetime.now(UTC).isoformat(timespec="seconds"),
492
+ }
493
+ (target_dir / _MANIFEST_FILENAME).write_text(
494
+ json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
495
+ )
496
+
497
+ def _print_agents(self) -> None:
498
+ rows = [
499
+ [t.name, t.kind, ", ".join(a.install_relpath for a in t.artifacts)] for t in _TARGETS
500
+ ]
501
+ self.output.print_table(
502
+ ["Agent", "Kind", "Install path (relative to --path)"],
503
+ rows,
504
+ title="Supported agents",
505
+ )
506
+
507
+ def _safe_project_root(self, path: str) -> Path:
508
+ project_root = Path(path).resolve()
509
+ if project_root.parent == project_root:
510
+ raise ValidationError(
511
+ f"Refusing to install skills at filesystem root: {project_root}",
512
+ field="path",
513
+ )
514
+ if project_root == Path.home().resolve():
515
+ raise ValidationError(
516
+ f"Refusing to install skills directly into the home directory: {project_root}. "
517
+ "Pass --path pointing at a project directory.",
518
+ field="path",
519
+ )
520
+ return project_root
521
+
522
+ def _normalize_agent(self, value: str) -> _AgentTarget:
523
+ key = value.strip().lower()
524
+ if key in _AGENT_LOOKUP:
525
+ return _AGENT_LOOKUP[key]
526
+
527
+ valid = "Codex, Claude, GitHub, or 1, 2, 3"
528
+ raise ValidationError(f"Invalid agent '{value}'. Choose {valid}.", field="agent")
529
+
530
+ def _templates_root(self) -> Path:
531
+ cli_dir = Path(__file__).resolve().parents[1]
532
+ return cli_dir / "templates" / "skills"
@@ -0,0 +1,113 @@
1
+ """Test command implementation — thin pytest wrapper."""
2
+
3
+ import subprocess # nosec: B404
4
+ import sys
5
+ import webbrowser
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from tenxgraph_api.cli.commands import BaseCommand
10
+ from tenxgraph_api.cli.core.config import ConfigManager
11
+
12
+
13
+ class TestCommand(BaseCommand):
14
+ """Run the project's test suite via pytest."""
15
+
16
+ def execute(
17
+ self,
18
+ path: str | None = None,
19
+ coverage: bool = False,
20
+ html: bool = False,
21
+ keyword: str | None = None,
22
+ verbose: bool = False,
23
+ quiet: bool = False,
24
+ extra_args: tuple[str, ...] = (),
25
+ **kwargs: Any,
26
+ ) -> int:
27
+ project_root = Path.cwd()
28
+ self.output.command_header("test", f"Running the project test suite in {project_root}")
29
+
30
+ timeline = self.output.timeline(
31
+ "Preparing the test run",
32
+ steps=(
33
+ ("config", "Resolving test configuration"),
34
+ ("command", "Building the pytest command"),
35
+ ),
36
+ )
37
+ with timeline:
38
+ with timeline.step("config") as step:
39
+ # Load optional overrides from 10xgraph.json
40
+ cfg: dict[str, Any] = {}
41
+ config_manager = ConfigManager()
42
+ discovered = config_manager.auto_discover_config()
43
+ if discovered:
44
+ try:
45
+ config_manager.load_config(str(discovered))
46
+ cfg = config_manager.get_test_config()
47
+ step.detail(str(discovered))
48
+ except Exception: # nosec: B110
49
+ self.logger.warning("Failed to load test configuration from %s", discovered)
50
+ step.detail(f"ignored unreadable config at {discovered}")
51
+ else:
52
+ step.detail("no 10xgraph.json found — using pytest defaults")
53
+
54
+ # Explicit CLI path wins; 10xgraph.json next; None = pytest auto-discovery
55
+ resolved_path: str | None = path or cfg.get("path") or None
56
+ resolved_coverage = coverage or cfg.get("coverage", False)
57
+ coverage_threshold: int | None = cfg.get("coverage_threshold")
58
+ location = str(project_root / resolved_path) if resolved_path else str(project_root)
59
+
60
+ with timeline.step("command") as step:
61
+ cmd = [sys.executable, "-m", "pytest"]
62
+ if resolved_path:
63
+ cmd.append(resolved_path)
64
+
65
+ if quiet and not verbose:
66
+ cmd.append("-q")
67
+ else:
68
+ cmd.append("-v")
69
+
70
+ if resolved_coverage:
71
+ cmd += [
72
+ "--cov=.",
73
+ "--cov-report=term-missing",
74
+ "--cov-report=html:htmlcov",
75
+ ]
76
+ if coverage_threshold is not None:
77
+ cmd.append(f"--cov-fail-under={coverage_threshold}")
78
+
79
+ if keyword:
80
+ cmd += ["-k", keyword]
81
+
82
+ cmd += list(extra_args)
83
+ step.detail(" ".join(["pytest", *cmd[3:]]))
84
+
85
+ self.logger.info("Running: %s", " ".join(cmd))
86
+
87
+ # No spinner around this: pytest owns the terminal while it streams its
88
+ # own progress, and a live region would fight it for the cursor.
89
+ result = subprocess.run(cmd, cwd=project_root, check=False) # nosec: B603 # noqa: S603
90
+
91
+ if result.returncode == 0:
92
+ self.output.completion_screen(
93
+ "Tests passed",
94
+ "The project test suite completed successfully",
95
+ details={
96
+ "Location": location,
97
+ "Coverage": "enabled" if resolved_coverage else "disabled",
98
+ },
99
+ next_steps=(
100
+ ["Open htmlcov/index.html to inspect the coverage report."]
101
+ if resolved_coverage
102
+ else []
103
+ ),
104
+ )
105
+ else:
106
+ self.output.error(f"Tests finished with exit code {result.returncode}.")
107
+
108
+ if html and resolved_coverage:
109
+ report_path = (project_root / "htmlcov" / "index.html").as_uri()
110
+ self.output.info(f"Opening coverage report: {report_path}", emoji=False)
111
+ webbrowser.open(report_path)
112
+
113
+ return result.returncode
@@ -0,0 +1,47 @@
1
+ """Version command implementation."""
2
+
3
+ from importlib.metadata import PackageNotFoundError
4
+ from importlib.metadata import version as _pkg_version
5
+ from typing import Any
6
+
7
+ from tenxgraph_api.cli.commands import BaseCommand
8
+ from tenxgraph_api.cli.constants import CLI_VERSION
9
+
10
+
11
+ class VersionCommand(BaseCommand):
12
+ """Command to display version information."""
13
+
14
+ def execute(self, **kwargs: Any) -> int:
15
+ """Execute the version command.
16
+
17
+ Returns:
18
+ Exit code
19
+ """
20
+ try:
21
+ self.output.command_header(
22
+ "version",
23
+ "Show 10xGraph CLI and package version info",
24
+ color="green",
25
+ )
26
+
27
+ core_version = self._core_version()
28
+
29
+ self.output.success(f"10xgraph-api\n Version: {CLI_VERSION}")
30
+ self.output.info(f"10xgraph (core)\n Version: {core_version}")
31
+
32
+ return 0
33
+
34
+ except Exception as e:
35
+ return self.handle_error(e)
36
+
37
+ @staticmethod
38
+ def _core_version() -> str:
39
+ """Resolve the installed core framework version.
40
+
41
+ Returns:
42
+ Version string, or ``"not installed"`` when the core package is absent.
43
+ """
44
+ try:
45
+ return _pkg_version("10xgraph")
46
+ except PackageNotFoundError:
47
+ return "not installed"