deepctl-core 0.2.6__tar.gz → 0.2.8__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.
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/PKG-INFO +1 -1
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/pyproject.toml +1 -1
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/auth.py +210 -34
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/config.py +4 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/skill_generator.py +69 -17
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/PKG-INFO +1 -1
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/README.md +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/setup.cfg +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/__init__.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/base_command.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/base_group_command.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/client.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/models.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/output.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/plugin_env.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/plugin_manager.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/py.typed +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/timing.py +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/SOURCES.txt +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/dependency_links.txt +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/requires.txt +0 -0
- {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/top_level.txt +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import time
|
|
5
5
|
import webbrowser
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
6
7
|
from urllib.parse import urlencode
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
@@ -20,6 +21,8 @@ console = Console()
|
|
|
20
21
|
AUTH_BASE_URL = os.getenv("DEEPGRAM_CLI_BASE_URL", "https://id.dx.deepgram.com")
|
|
21
22
|
DEVICE_CODE_URL = f"{AUTH_BASE_URL}/device/code"
|
|
22
23
|
TOKEN_POLL_URL = f"{AUTH_BASE_URL}/device/token"
|
|
24
|
+
TOKEN_REFRESH_URL = f"{AUTH_BASE_URL}/token"
|
|
25
|
+
DG_CREDENTIALS_URL = f"{AUTH_BASE_URL}/credentials/token"
|
|
23
26
|
|
|
24
27
|
# Registered OIDC client ID for the CLI
|
|
25
28
|
CLIENT_ID = "deepgram-cli"
|
|
@@ -43,6 +46,7 @@ class TokenResponse(BaseModel):
|
|
|
43
46
|
|
|
44
47
|
access_token: str
|
|
45
48
|
project_id: str
|
|
49
|
+
refresh_token: str | None = None # Present in new JWT-based device flow
|
|
46
50
|
token_type: str | None = None
|
|
47
51
|
expires_in: int | None = None
|
|
48
52
|
scope: str | None = None
|
|
@@ -105,11 +109,12 @@ class AuthManager:
|
|
|
105
109
|
"""
|
|
106
110
|
profile_name = profile_name or self.config.profile or "default"
|
|
107
111
|
|
|
108
|
-
# Check keyring for API key
|
|
112
|
+
# Check keyring for direct API key or JWT (both indicate logged-in state)
|
|
109
113
|
has_api_key = False
|
|
110
114
|
try:
|
|
111
115
|
api_key = keyring.get_password(KEYRING_SERVICE, f"api-key.{profile_name}")
|
|
112
|
-
|
|
116
|
+
jwt = keyring.get_password(KEYRING_SERVICE, f"jwt.{profile_name}")
|
|
117
|
+
has_api_key = bool(api_key or jwt)
|
|
113
118
|
except Exception:
|
|
114
119
|
pass
|
|
115
120
|
|
|
@@ -150,32 +155,47 @@ class AuthManager:
|
|
|
150
155
|
return bool(os.getenv("DEEPGRAM_API_KEY") and os.getenv("DEEPGRAM_PROJECT_ID"))
|
|
151
156
|
|
|
152
157
|
def get_api_key(self, ignore_env: bool = False) -> str | None:
|
|
153
|
-
"""Get API key following precedence: explicit >
|
|
158
|
+
"""Get API key following precedence: explicit > JWT flow > legacy keyring > env.
|
|
159
|
+
|
|
160
|
+
When the user logged in via device flow with the new JWT-based server,
|
|
161
|
+
this method transparently refreshes the JWT if expired and exchanges it
|
|
162
|
+
for a Deepgram API token (dg_token). All callers receive a usable
|
|
163
|
+
Deepgram token regardless of which auth path was used.
|
|
164
|
+
|
|
165
|
+
Passing --api-key (or DEEPGRAM_API_KEY) bypasses the JWT flow entirely,
|
|
166
|
+
which is the correct behaviour for CI/CD and direct key usage.
|
|
154
167
|
|
|
155
168
|
Args:
|
|
156
169
|
ignore_env: If True, don't check environment variables
|
|
157
170
|
"""
|
|
158
|
-
# Explicit credentials
|
|
171
|
+
# Explicit credentials (--api-key flag, CI/CD) bypass the JWT flow.
|
|
159
172
|
if self.explicit_api_key:
|
|
160
173
|
return self.explicit_api_key
|
|
161
174
|
|
|
162
|
-
# Check profile credentials next
|
|
163
175
|
profile_name = self.config.profile or "default"
|
|
164
176
|
|
|
165
|
-
#
|
|
177
|
+
# JWT-based flow: present when logged in via device flow with new server.
|
|
178
|
+
try:
|
|
179
|
+
jwt = keyring.get_password(KEYRING_SERVICE, f"jwt.{profile_name}")
|
|
180
|
+
if jwt:
|
|
181
|
+
return self._get_dg_token_via_jwt(jwt, profile_name)
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# Legacy path: direct API key in keyring (--api-key login, old device flow).
|
|
166
186
|
try:
|
|
167
187
|
api_key = keyring.get_password(KEYRING_SERVICE, f"api-key.{profile_name}")
|
|
168
188
|
if api_key:
|
|
169
189
|
return api_key
|
|
170
190
|
except Exception:
|
|
171
|
-
pass
|
|
191
|
+
pass
|
|
172
192
|
|
|
173
|
-
#
|
|
193
|
+
# Config file fallback (keyring unavailable on this machine).
|
|
174
194
|
current_profile = self.config.get_profile(profile_name)
|
|
175
195
|
if current_profile.api_key:
|
|
176
196
|
return current_profile.api_key
|
|
177
197
|
|
|
178
|
-
# Environment variable
|
|
198
|
+
# Environment variable — lowest priority.
|
|
179
199
|
if not ignore_env:
|
|
180
200
|
api_key = os.getenv("DEEPGRAM_API_KEY")
|
|
181
201
|
if api_key:
|
|
@@ -596,29 +616,66 @@ class AuthManager:
|
|
|
596
616
|
raise AuthenticationError("Authentication timed out")
|
|
597
617
|
|
|
598
618
|
def _store_token(self, token_response: TokenResponse) -> None:
|
|
599
|
-
"""Store authentication token.
|
|
600
|
-
# The access_token from dx-id is the Deepgram API key
|
|
601
|
-
api_key = token_response.access_token
|
|
602
|
-
project_id = token_response.project_id
|
|
619
|
+
"""Store authentication token.
|
|
603
620
|
|
|
621
|
+
Handles two server response shapes:
|
|
622
|
+
- New JWT flow: access_token is a short-lived JWT (expires_in=900),
|
|
623
|
+
refresh_token present. Stores JWT + refresh_token in keyring.
|
|
624
|
+
- Legacy flow: access_token is a Deepgram API key directly.
|
|
625
|
+
Stored under api-key.{profile} as before.
|
|
626
|
+
"""
|
|
604
627
|
profile_name = self.config.profile or "default"
|
|
605
|
-
keyring_available = False
|
|
606
628
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
629
|
+
if token_response.refresh_token:
|
|
630
|
+
# New JWT-based device flow.
|
|
631
|
+
jwt = token_response.access_token
|
|
632
|
+
refresh_token = token_response.refresh_token
|
|
633
|
+
expires_in = token_response.expires_in or 900
|
|
634
|
+
expires_at = (
|
|
635
|
+
datetime.now(timezone.utc) + timedelta(seconds=expires_in)
|
|
636
|
+
).isoformat()
|
|
615
637
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
638
|
+
try:
|
|
639
|
+
# Clear any stale direct API key so get_api_key() doesn't
|
|
640
|
+
# return a cached dg_token from a previous session.
|
|
641
|
+
keyring.delete_password(KEYRING_SERVICE, f"api-key.{profile_name}")
|
|
642
|
+
except Exception:
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
keyring.set_password(KEYRING_SERVICE, f"jwt.{profile_name}", jwt)
|
|
647
|
+
keyring.set_password(
|
|
648
|
+
KEYRING_SERVICE, f"refresh-token.{profile_name}", refresh_token
|
|
649
|
+
)
|
|
650
|
+
console.print("[green]✓[/green] Session stored securely in system keyring")
|
|
651
|
+
except Exception as e:
|
|
652
|
+
console.print(f"[yellow]Warning:[/yellow] Could not store in keyring: {e}")
|
|
653
|
+
|
|
654
|
+
# Store expiry timestamps and project ID in config (non-sensitive).
|
|
655
|
+
profile = self.config.get_profile(profile_name)
|
|
656
|
+
profile.jwt_expires_at = expires_at
|
|
657
|
+
profile.project_id = token_response.project_id
|
|
658
|
+
self.config.save()
|
|
659
|
+
|
|
660
|
+
else:
|
|
661
|
+
# Legacy flow: access_token is the Deepgram API key directly.
|
|
662
|
+
api_key = token_response.access_token
|
|
663
|
+
project_id = token_response.project_id
|
|
664
|
+
keyring_available = False
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
keyring.set_password(KEYRING_SERVICE, f"api-key.{profile_name}", api_key)
|
|
668
|
+
console.print("[green]✓[/green] API key stored securely in system keyring")
|
|
669
|
+
keyring_available = True
|
|
670
|
+
except Exception as e:
|
|
671
|
+
console.print(f"[yellow]Warning:[/yellow] Could not store in keyring: {e}")
|
|
672
|
+
console.print("API key will be stored in config file instead")
|
|
673
|
+
|
|
674
|
+
self.config.create_profile(
|
|
675
|
+
profile_name,
|
|
676
|
+
api_key=api_key if not keyring_available else None,
|
|
677
|
+
project_id=project_id,
|
|
678
|
+
)
|
|
622
679
|
|
|
623
680
|
def logout(self, keep_config: bool = False) -> None:
|
|
624
681
|
"""Logout user and clear credentials.
|
|
@@ -629,12 +686,17 @@ class AuthManager:
|
|
|
629
686
|
"""
|
|
630
687
|
profile_name = self.config.profile or "default"
|
|
631
688
|
|
|
632
|
-
# Clear
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
689
|
+
# Clear all credential types from keyring.
|
|
690
|
+
for keyring_key in (
|
|
691
|
+
f"api-key.{profile_name}",
|
|
692
|
+
f"jwt.{profile_name}",
|
|
693
|
+
f"refresh-token.{profile_name}",
|
|
694
|
+
):
|
|
695
|
+
try:
|
|
696
|
+
keyring.delete_password(KEYRING_SERVICE, keyring_key)
|
|
697
|
+
except Exception:
|
|
698
|
+
pass
|
|
699
|
+
console.print("[dim]Cleared credentials from system keyring[/dim]")
|
|
638
700
|
|
|
639
701
|
if keep_config:
|
|
640
702
|
# Clear sensitive data from config but keep profile
|
|
@@ -687,6 +749,120 @@ class AuthManager:
|
|
|
687
749
|
current_profile=self.config.profile or self.config._config.default_profile,
|
|
688
750
|
)
|
|
689
751
|
|
|
752
|
+
def _get_dg_token_via_jwt(self, jwt: str, profile_name: str) -> str:
|
|
753
|
+
"""Return a valid Deepgram API token, refreshing as needed.
|
|
754
|
+
|
|
755
|
+
Checks the cached dg_token first; refreshes the JWT if expired; then
|
|
756
|
+
exchanges the JWT for a fresh dg_token when the cached one is stale.
|
|
757
|
+
"""
|
|
758
|
+
profile = self.config.get_profile(profile_name)
|
|
759
|
+
now = datetime.now(timezone.utc)
|
|
760
|
+
|
|
761
|
+
# Return cached dg_token if it is still valid.
|
|
762
|
+
try:
|
|
763
|
+
dg_token = keyring.get_password(KEYRING_SERVICE, f"api-key.{profile_name}")
|
|
764
|
+
if dg_token and profile.dg_token_expires_at:
|
|
765
|
+
expires = datetime.fromisoformat(profile.dg_token_expires_at)
|
|
766
|
+
if now < expires:
|
|
767
|
+
return dg_token
|
|
768
|
+
except Exception:
|
|
769
|
+
pass
|
|
770
|
+
|
|
771
|
+
# Refresh the JWT if it has expired.
|
|
772
|
+
current_jwt = jwt
|
|
773
|
+
if profile.jwt_expires_at:
|
|
774
|
+
jwt_expires = datetime.fromisoformat(profile.jwt_expires_at)
|
|
775
|
+
if now >= jwt_expires:
|
|
776
|
+
current_jwt = self._refresh_jwt(profile_name)
|
|
777
|
+
|
|
778
|
+
return self._exchange_for_dg_token(current_jwt, profile_name)
|
|
779
|
+
|
|
780
|
+
def _refresh_jwt(self, profile_name: str) -> str:
|
|
781
|
+
"""Exchange a refresh token for a new JWT."""
|
|
782
|
+
try:
|
|
783
|
+
refresh_token = keyring.get_password(
|
|
784
|
+
KEYRING_SERVICE, f"refresh-token.{profile_name}"
|
|
785
|
+
)
|
|
786
|
+
except Exception:
|
|
787
|
+
refresh_token = None
|
|
788
|
+
|
|
789
|
+
if not refresh_token:
|
|
790
|
+
raise AuthenticationError(
|
|
791
|
+
"Session expired and no refresh token is available. "
|
|
792
|
+
"Please run 'dg login' again."
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
try:
|
|
796
|
+
response = self.client.post(
|
|
797
|
+
TOKEN_REFRESH_URL,
|
|
798
|
+
json={"grant_type": "refresh_token", "refresh_token": refresh_token},
|
|
799
|
+
)
|
|
800
|
+
except httpx.RequestError as e:
|
|
801
|
+
raise AuthenticationError(f"Network error during token refresh: {e}")
|
|
802
|
+
|
|
803
|
+
if response.status_code not in (200, 201):
|
|
804
|
+
raise AuthenticationError(
|
|
805
|
+
f"Token refresh failed (HTTP {response.status_code}). "
|
|
806
|
+
"Please run 'dg login' again."
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
data = response.json()
|
|
810
|
+
new_jwt: str = data["access_token"]
|
|
811
|
+
new_refresh: str = data.get("refresh_token", refresh_token)
|
|
812
|
+
expires_in: int = data.get("expires_in", 900)
|
|
813
|
+
expires_at = (
|
|
814
|
+
datetime.now(timezone.utc) + timedelta(seconds=expires_in)
|
|
815
|
+
).isoformat()
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
keyring.set_password(KEYRING_SERVICE, f"jwt.{profile_name}", new_jwt)
|
|
819
|
+
if new_refresh != refresh_token:
|
|
820
|
+
keyring.set_password(
|
|
821
|
+
KEYRING_SERVICE, f"refresh-token.{profile_name}", new_refresh
|
|
822
|
+
)
|
|
823
|
+
except Exception:
|
|
824
|
+
pass
|
|
825
|
+
|
|
826
|
+
profile = self.config.get_profile(profile_name)
|
|
827
|
+
profile.jwt_expires_at = expires_at
|
|
828
|
+
self.config.save()
|
|
829
|
+
|
|
830
|
+
return new_jwt
|
|
831
|
+
|
|
832
|
+
def _exchange_for_dg_token(self, jwt: str, profile_name: str) -> str:
|
|
833
|
+
"""Exchange a JWT for a Deepgram API token (dg_token)."""
|
|
834
|
+
try:
|
|
835
|
+
response = self.client.post(
|
|
836
|
+
DG_CREDENTIALS_URL,
|
|
837
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
838
|
+
)
|
|
839
|
+
except httpx.RequestError as e:
|
|
840
|
+
raise AuthenticationError(f"Network error during credentials exchange: {e}")
|
|
841
|
+
|
|
842
|
+
if response.status_code not in (200, 201):
|
|
843
|
+
raise AuthenticationError(
|
|
844
|
+
f"Credentials exchange failed (HTTP {response.status_code}). "
|
|
845
|
+
"Please run 'dg login' again."
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
data = response.json()
|
|
849
|
+
dg_token: str = data["dg_token"]
|
|
850
|
+
expires_in: int = data.get("expires_in", 86400)
|
|
851
|
+
expires_at = (
|
|
852
|
+
datetime.now(timezone.utc) + timedelta(seconds=expires_in)
|
|
853
|
+
).isoformat()
|
|
854
|
+
|
|
855
|
+
try:
|
|
856
|
+
keyring.set_password(KEYRING_SERVICE, f"api-key.{profile_name}", dg_token)
|
|
857
|
+
except Exception:
|
|
858
|
+
pass # Non-fatal: will re-exchange next time
|
|
859
|
+
|
|
860
|
+
profile = self.config.get_profile(profile_name)
|
|
861
|
+
profile.dg_token_expires_at = expires_at
|
|
862
|
+
self.config.save()
|
|
863
|
+
|
|
864
|
+
return dg_token
|
|
865
|
+
|
|
690
866
|
def __del__(self) -> None:
|
|
691
867
|
"""Cleanup HTTP client."""
|
|
692
868
|
if hasattr(self, "client"):
|
|
@@ -15,6 +15,10 @@ class ProfileConfig(BaseModel):
|
|
|
15
15
|
api_key: str | None = None
|
|
16
16
|
project_id: str | None = None
|
|
17
17
|
base_url: str = "https://api.deepgram.com"
|
|
18
|
+
# JWT-based auth (device flow with new server): expiry timestamps stored
|
|
19
|
+
# here (non-sensitive); the tokens themselves live in the keyring.
|
|
20
|
+
jwt_expires_at: str | None = None
|
|
21
|
+
dg_token_expires_at: str | None = None
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
class OutputConfig(BaseModel):
|
|
@@ -84,7 +84,7 @@ def fetch_repo_skills(force: bool = False) -> dict[str, str]:
|
|
|
84
84
|
pass
|
|
85
85
|
|
|
86
86
|
base = f"https://raw.githubusercontent.com/{_SKILLS_REPO}/{_SKILLS_BRANCH}"
|
|
87
|
-
skill_names = ["api", "docs", "
|
|
87
|
+
skill_names = ["api", "docs", "setup-mcp", "starters"]
|
|
88
88
|
skills: dict[str, str] = {}
|
|
89
89
|
|
|
90
90
|
_REPO_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -659,24 +659,24 @@ class SkillGenerator(ABC):
|
|
|
659
659
|
Mapping of file path -> content string
|
|
660
660
|
"""
|
|
661
661
|
|
|
662
|
-
def install(
|
|
663
|
-
|
|
664
|
-
#
|
|
662
|
+
def install(
|
|
663
|
+
self,
|
|
664
|
+
commands: list[CommandMetadata], # noqa: ARG002
|
|
665
|
+
version: str, # noqa: ARG002
|
|
666
|
+
) -> list[Path]:
|
|
667
|
+
"""Fetch skills from deepgram/skills repo and install them."""
|
|
665
668
|
repo_skills = fetch_repo_skills(force=True)
|
|
669
|
+
if not repo_skills:
|
|
670
|
+
return []
|
|
671
|
+
return self._write_repo_skills(repo_skills)
|
|
666
672
|
|
|
667
|
-
|
|
673
|
+
def _write_repo_skills(self, repo_skills: dict[str, str]) -> list[Path]:
|
|
674
|
+
"""Write combined repo skill content to this tool's skill paths."""
|
|
675
|
+
combined = "\n\n---\n\n".join(repo_skills.values())
|
|
668
676
|
written: list[Path] = []
|
|
669
|
-
for path
|
|
670
|
-
# Append repo skill content to the generated file
|
|
671
|
-
if repo_skills:
|
|
672
|
-
sections = [
|
|
673
|
-
f"\n\n## {name.title()}\n\n{body}"
|
|
674
|
-
for name, body in repo_skills.items()
|
|
675
|
-
]
|
|
676
|
-
content += "\n".join(sections)
|
|
677
|
-
|
|
677
|
+
for path in self.get_skill_paths():
|
|
678
678
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
679
|
-
path.write_text(
|
|
679
|
+
path.write_text(combined)
|
|
680
680
|
written.append(path)
|
|
681
681
|
return written
|
|
682
682
|
|
|
@@ -705,6 +705,10 @@ class ClaudeCodeGenerator(SkillGenerator):
|
|
|
705
705
|
cli_name = "claude"
|
|
706
706
|
display_name = "Claude Code"
|
|
707
707
|
|
|
708
|
+
@property
|
|
709
|
+
def _skill_dir(self) -> Path:
|
|
710
|
+
return Path.home() / ".claude" / "commands" / "deepgram"
|
|
711
|
+
|
|
708
712
|
def detect(self) -> bool:
|
|
709
713
|
return (
|
|
710
714
|
Path.home().joinpath(".claude").is_dir()
|
|
@@ -712,13 +716,37 @@ class ClaudeCodeGenerator(SkillGenerator):
|
|
|
712
716
|
)
|
|
713
717
|
|
|
714
718
|
def get_skill_paths(self) -> list[Path]:
|
|
715
|
-
return [
|
|
719
|
+
return [
|
|
720
|
+
self._skill_dir / f"{name}.md"
|
|
721
|
+
for name in ["api", "docs", "setup-mcp", "starters"]
|
|
722
|
+
]
|
|
716
723
|
|
|
717
724
|
def generate(
|
|
718
725
|
self, commands: list[CommandMetadata], version: str
|
|
719
726
|
) -> dict[Path, str]:
|
|
720
727
|
content = render_skill_content(commands, version, include_frontmatter=True)
|
|
721
|
-
return {self.
|
|
728
|
+
return {self._skill_dir / "deepgram.md": content}
|
|
729
|
+
|
|
730
|
+
def _write_repo_skills(self, repo_skills: dict[str, str]) -> list[Path]:
|
|
731
|
+
self._skill_dir.mkdir(parents=True, exist_ok=True)
|
|
732
|
+
written: list[Path] = []
|
|
733
|
+
for name, content in repo_skills.items():
|
|
734
|
+
path = self._skill_dir / f"{name}.md"
|
|
735
|
+
path.write_text(content)
|
|
736
|
+
written.append(path)
|
|
737
|
+
return written
|
|
738
|
+
|
|
739
|
+
def remove(self) -> list[Path]:
|
|
740
|
+
removed: list[Path] = []
|
|
741
|
+
if self._skill_dir.exists():
|
|
742
|
+
for f in self._skill_dir.glob("*.md"):
|
|
743
|
+
f.unlink()
|
|
744
|
+
removed.append(f)
|
|
745
|
+
try:
|
|
746
|
+
self._skill_dir.rmdir()
|
|
747
|
+
except OSError:
|
|
748
|
+
pass
|
|
749
|
+
return removed
|
|
722
750
|
|
|
723
751
|
|
|
724
752
|
class CodexGenerator(SkillGenerator):
|
|
@@ -758,6 +786,14 @@ class CodexGenerator(SkillGenerator):
|
|
|
758
786
|
return before + section + after.lstrip("\n")
|
|
759
787
|
return existing.rstrip("\n") + "\n\n" + section
|
|
760
788
|
|
|
789
|
+
def _write_repo_skills(self, repo_skills: dict[str, str]) -> list[Path]:
|
|
790
|
+
combined = "\n\n---\n\n".join(repo_skills.values())
|
|
791
|
+
wrapped = f"{self._BEGIN}\n{combined}\n{self._END}\n"
|
|
792
|
+
path = self.get_skill_paths()[0]
|
|
793
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
794
|
+
path.write_text(self._merge(path, wrapped))
|
|
795
|
+
return [path]
|
|
796
|
+
|
|
761
797
|
def remove(self) -> list[Path]:
|
|
762
798
|
path = self.get_skill_paths()[0]
|
|
763
799
|
if not path.exists():
|
|
@@ -813,6 +849,14 @@ class GeminiGenerator(SkillGenerator):
|
|
|
813
849
|
return before + section + after.lstrip("\n")
|
|
814
850
|
return existing.rstrip("\n") + "\n\n" + section
|
|
815
851
|
|
|
852
|
+
def _write_repo_skills(self, repo_skills: dict[str, str]) -> list[Path]:
|
|
853
|
+
combined = "\n\n---\n\n".join(repo_skills.values())
|
|
854
|
+
wrapped = f"{self._BEGIN}\n{combined}\n{self._END}\n"
|
|
855
|
+
path = self.get_skill_paths()[0]
|
|
856
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
857
|
+
path.write_text(self._merge(path, wrapped))
|
|
858
|
+
return [path]
|
|
859
|
+
|
|
816
860
|
def remove(self) -> list[Path]:
|
|
817
861
|
path = self.get_skill_paths()[0]
|
|
818
862
|
if not path.exists():
|
|
@@ -954,6 +998,14 @@ class OpenCodeGenerator(SkillGenerator):
|
|
|
954
998
|
return before + section + after.lstrip("\n")
|
|
955
999
|
return existing.rstrip("\n") + "\n\n" + section
|
|
956
1000
|
|
|
1001
|
+
def _write_repo_skills(self, repo_skills: dict[str, str]) -> list[Path]:
|
|
1002
|
+
combined = "\n\n---\n\n".join(repo_skills.values())
|
|
1003
|
+
wrapped = f"{self._BEGIN}\n{combined}\n{self._END}\n"
|
|
1004
|
+
path = self.get_skill_paths()[0]
|
|
1005
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1006
|
+
path.write_text(self._merge(path, wrapped))
|
|
1007
|
+
return [path]
|
|
1008
|
+
|
|
957
1009
|
def remove(self) -> list[Path]:
|
|
958
1010
|
path = self.get_skill_paths()[0]
|
|
959
1011
|
if not path.exists():
|
|
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
|