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.
Files changed (22) hide show
  1. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/PKG-INFO +1 -1
  2. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/pyproject.toml +1 -1
  3. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/auth.py +210 -34
  4. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/config.py +4 -0
  5. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/skill_generator.py +69 -17
  6. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/PKG-INFO +1 -1
  7. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/README.md +0 -0
  8. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/setup.cfg +0 -0
  9. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/__init__.py +0 -0
  10. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/base_command.py +0 -0
  11. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/base_group_command.py +0 -0
  12. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/client.py +0 -0
  13. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/models.py +0 -0
  14. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/output.py +0 -0
  15. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/plugin_env.py +0 -0
  16. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/plugin_manager.py +0 -0
  17. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/py.typed +0 -0
  18. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core/timing.py +0 -0
  19. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/SOURCES.txt +0 -0
  20. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/dependency_links.txt +0 -0
  21. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/requires.txt +0 -0
  22. {deepctl_core-0.2.6 → deepctl_core-0.2.8}/src/deepctl_core.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-core
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Core components for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepctl-core"
7
- version = "0.2.6" # x-release-please-version
7
+ version = "0.2.8" # x-release-please-version
8
8
  description = "Core components for deepctl"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- has_api_key = bool(api_key)
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 > profile > env.
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 have highest priority
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
- # Check keyring first
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 # Keyring not available or error
191
+ pass
172
192
 
173
- # Check config file
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 has lowest priority
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
- try:
608
- keyring.set_password(KEYRING_SERVICE, f"api-key.{profile_name}", api_key)
609
- # Don't store project ID in keyring - it goes in profile config only
610
- console.print("[green]✓[/green] API key stored securely in system keyring")
611
- keyring_available = True
612
- except Exception as e:
613
- console.print(f"[yellow]Warning:[/yellow] Could not store in keyring: {e}")
614
- console.print("API key will be stored in config file instead")
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
- # Update config - only store API key if keyring is not available
617
- self.config.create_profile(
618
- profile_name,
619
- api_key=api_key if not keyring_available else None,
620
- project_id=project_id, # Always store project ID in config
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 keyring (API key only)
633
- try:
634
- keyring.delete_password(KEYRING_SERVICE, f"api-key.{profile_name}")
635
- console.print("[dim]Cleared API key from system keyring[/dim]")
636
- except Exception:
637
- pass # Ignore errors if not stored
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", "starters", "mcp"]
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(self, commands: list[CommandMetadata], version: str) -> list[Path]:
663
- """Generate and write skill files with latest repo skills."""
664
- # Always fetch fresh skills from deepgram/skills repo
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
- files = self.generate(commands, version)
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, content in files.items():
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(content)
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 [Path.home() / ".claude" / "commands" / "deepctl.md"]
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.get_skill_paths()[0]: content}
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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-core
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Core components for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
File without changes
File without changes