From 6a3851f6c1fbcdb343282a61ff66fab3cc1d6186 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:48:33 -0500 Subject: [PATCH 1/6] Remove Template Version and Released from version output Templates are now bundled with the CLI, so showing them as separate artifacts with their own version and release date is no longer accurate. This also removes the GitHub API call that fetched the latest release, making the version command faster and eliminating a network dependency. --- src/specify_cli/__init__.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index fbe1bc033f..7e0229a591 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -1429,45 +1429,11 @@ def version(): except Exception: pass - # Fetch latest template release version - repo_owner = "github" - repo_name = "spec-kit" - api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" - - template_version = "unknown" - release_date = "unknown" - - try: - response = client.get( - api_url, - timeout=10, - follow_redirects=True, - headers=_github_auth_headers(), - ) - if response.status_code == 200: - release_data = response.json() - template_version = release_data.get("tag_name", "unknown") - # Remove 'v' prefix if present - if template_version.startswith("v"): - template_version = template_version[1:] - release_date = release_data.get("published_at", "unknown") - if release_date != "unknown": - # Format the date nicely - try: - dt = datetime.fromisoformat(release_date.replace('Z', '+00:00')) - release_date = dt.strftime("%Y-%m-%d") - except Exception: - pass - except Exception: - pass - info_table = Table(show_header=False, box=None, padding=(0, 2)) info_table.add_column("Key", style="cyan", justify="right") info_table.add_column("Value", style="white") info_table.add_row("CLI Version", cli_version) - info_table.add_row("Template Version", template_version) - info_table.add_row("Released", release_date) info_table.add_row("", "") info_table.add_row("Python", platform.python_version()) info_table.add_row("Platform", platform.system()) From 79b50601bca6db5e1a938d47f1575767eafe96a8 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:50:45 -0500 Subject: [PATCH 2/6] Remove unused datetime import --- src/specify_cli/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 7e0229a591..9e359c4502 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -53,7 +53,6 @@ import readchar import ssl import truststore -from datetime import datetime ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) client = httpx.Client(verify=ssl_context) From fa0f76049914e9ddba667059cab15e26d270fcc8 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:56:22 -0500 Subject: [PATCH 3/6] fix: inject user-invocable: true into Claude skill frontmatter The SkillsIntegration.setup() builds frontmatter manually without user-invocable. Add post-processing injection in ClaudeIntegration.setup(), matching the existing pattern for disable-model-invocation. --- .../integrations/claude/__init__.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 9eb3214614..9b58305794 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -111,6 +111,38 @@ def _build_skill_fm(self, name: str, description: str, source: str) -> dict: self.key, name, description, source ) + @staticmethod + def _inject_user_invocable(content: str) -> str: + """Insert ``user-invocable: true`` before the closing ``---``.""" + lines = content.splitlines(keepends=True) + + # Pre-scan: bail out if already present in frontmatter + dash_count = 0 + for line in lines: + stripped = line.rstrip("\n\r") + if stripped == "---": + dash_count += 1 + if dash_count == 2: + break + continue + if dash_count == 1 and stripped.startswith("user-invocable:"): + return content + + # Inject before the closing --- of frontmatter + out: list[str] = [] + dash_count = 0 + injected = False + for line in lines: + stripped = line.rstrip("\n\r") + if stripped == "---": + dash_count += 1 + if dash_count == 2 and not injected: + eol = "\r\n" if line.endswith("\r\n") else "\n" + out.append(f"user-invocable: true{eol}") + injected = True + out.append(line) + return "".join(out) + @staticmethod def _inject_disable_model_invocation(content: str) -> str: """Insert ``disable-model-invocation: true`` before the closing ``---``.""" @@ -168,8 +200,11 @@ def setup( content_bytes = path.read_bytes() content = content_bytes.decode("utf-8") + # Inject user-invocable: true (Claude skills are accessible via /command) + updated = self._inject_user_invocable(content) + # Inject disable-model-invocation: true (Claude skills run only when invoked) - updated = self._inject_disable_model_invocation(content) + updated = self._inject_disable_model_invocation(updated) # Inject argument-hint if available for this skill skill_dir_name = path.parent.name # e.g. "speckit-plan" From a68bcb9fd0f778c11f56ba5635cb632613ed6929 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:09:14 -0500 Subject: [PATCH 4/6] refactor: address review feedback - Factor _inject_user_invocable and _inject_disable_model_invocation into a shared _inject_frontmatter_flag(key, value) helper - Remove unused httpx, ssl, truststore imports and globals - Remove unused _github_token and _github_auth_headers helpers - Update setup() docstring to mention user-invocable --- src/specify_cli/__init__.py | 15 ------ .../integrations/claude/__init__.py | 46 +++---------------- 2 files changed, 7 insertions(+), 54 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 9e359c4502..4afd87ea82 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -39,7 +39,6 @@ from typing import Any, Optional, Tuple import typer -import httpx from rich.console import Console from rich.panel import Panel from rich.text import Text @@ -51,20 +50,6 @@ # For cross-platform keyboard input import readchar -import ssl -import truststore - -ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) -client = httpx.Client(verify=ssl_context) - -def _github_token(cli_token: str | None = None) -> str | None: - """Return sanitized GitHub token (cli arg takes precedence) or None.""" - return ((cli_token or os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN") or "").strip()) or None - -def _github_auth_headers(cli_token: str | None = None) -> dict: - """Return Authorization header dict only when a non-empty token exists.""" - token = _github_token(cli_token) - return {"Authorization": f"Bearer {token}"} if token else {} def _build_agent_config() -> dict[str, dict[str, Any]]: """Derive AGENT_CONFIG from INTEGRATION_REGISTRY.""" diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index 9b58305794..a683b91351 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -112,8 +112,8 @@ def _build_skill_fm(self, name: str, description: str, source: str) -> dict: ) @staticmethod - def _inject_user_invocable(content: str) -> str: - """Insert ``user-invocable: true`` before the closing ``---``.""" + def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str: + """Insert ``key: value`` before the closing ``---`` if not already present.""" lines = content.splitlines(keepends=True) # Pre-scan: bail out if already present in frontmatter @@ -125,7 +125,7 @@ def _inject_user_invocable(content: str) -> str: if dash_count == 2: break continue - if dash_count == 1 and stripped.startswith("user-invocable:"): + if dash_count == 1 and stripped.startswith(f"{key}:"): return content # Inject before the closing --- of frontmatter @@ -138,39 +138,7 @@ def _inject_user_invocable(content: str) -> str: dash_count += 1 if dash_count == 2 and not injected: eol = "\r\n" if line.endswith("\r\n") else "\n" - out.append(f"user-invocable: true{eol}") - injected = True - out.append(line) - return "".join(out) - - @staticmethod - def _inject_disable_model_invocation(content: str) -> str: - """Insert ``disable-model-invocation: true`` before the closing ``---``.""" - lines = content.splitlines(keepends=True) - - # Pre-scan: bail out if already present in frontmatter - dash_count = 0 - for line in lines: - stripped = line.rstrip("\n\r") - if stripped == "---": - dash_count += 1 - if dash_count == 2: - break - continue - if dash_count == 1 and stripped.startswith("disable-model-invocation:"): - return content - - # Inject before the closing --- of frontmatter - out: list[str] = [] - dash_count = 0 - injected = False - for line in lines: - stripped = line.rstrip("\n\r") - if stripped == "---": - dash_count += 1 - if dash_count == 2 and not injected: - eol = "\r\n" if line.endswith("\r\n") else "\n" - out.append(f"disable-model-invocation: true{eol}") + out.append(f"{key}: {value}{eol}") injected = True out.append(line) return "".join(out) @@ -182,7 +150,7 @@ def setup( parsed_options: dict[str, Any] | None = None, **opts: Any, ) -> list[Path]: - """Install Claude skills, then inject argument-hint and disable-model-invocation.""" + """Install Claude skills, then inject user-invocable, disable-model-invocation, and argument-hint.""" created = super().setup(project_root, manifest, parsed_options, **opts) # Post-process generated skill files @@ -201,10 +169,10 @@ def setup( content = content_bytes.decode("utf-8") # Inject user-invocable: true (Claude skills are accessible via /command) - updated = self._inject_user_invocable(content) + updated = self._inject_frontmatter_flag(content, "user-invocable") # Inject disable-model-invocation: true (Claude skills run only when invoked) - updated = self._inject_disable_model_invocation(updated) + updated = self._inject_frontmatter_flag(updated, "disable-model-invocation") # Inject argument-hint if available for this skill skill_dir_name = path.parent.name # e.g. "speckit-plan" From 82ae1916371f81f4d2a0df48a3209a15cf1fbe13 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:21:52 -0500 Subject: [PATCH 5/6] chore: remove httpx and truststore from dependencies Both are no longer used after removing the GitHub API call from the version command. Removes from PEP 723 script header and pyproject.toml. --- pyproject.toml | 2 -- src/specify_cli/__init__.py | 1 - 2 files changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4eb8b2f978..02a4cdd7dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,8 @@ dependencies = [ "typer", "click>=8.1", "rich", - "httpx[socks]", "platformdirs", "readchar", - "truststore>=0.10.4", "pyyaml>=6.0", "packaging>=23.0", "pathspec>=0.12.0", diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 4afd87ea82..28831e6cd2 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -6,7 +6,6 @@ # "rich", # "platformdirs", # "readchar", -# "httpx", # "json5", # ] # /// From baded66b2840e6f25f90f48bf4835db62859143c Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:29:42 -0500 Subject: [PATCH 6/6] fix: match EOL detection style in _inject_frontmatter_flag Handle \r\n, \n, and no-newline cases consistently with inject_argument_hint's pattern. --- src/specify_cli/integrations/claude/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/integrations/claude/__init__.py b/src/specify_cli/integrations/claude/__init__.py index a683b91351..31972c4b0e 100644 --- a/src/specify_cli/integrations/claude/__init__.py +++ b/src/specify_cli/integrations/claude/__init__.py @@ -137,7 +137,12 @@ def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str if stripped == "---": dash_count += 1 if dash_count == 2 and not injected: - eol = "\r\n" if line.endswith("\r\n") else "\n" + if line.endswith("\r\n"): + eol = "\r\n" + elif line.endswith("\n"): + eol = "\n" + else: + eol = "" out.append(f"{key}: {value}{eol}") injected = True out.append(line)